Skip to main content
iOS native modules allow you to write Objective-C or Swift code that can be called from your React Native JavaScript code.

Basic Module Structure

An iOS native module consists of:
  1. A class implementing the RCTBridgeModule protocol
  2. Methods exported with RCT_EXPORT_METHOD macro
  3. Module registration with RCT_EXPORT_MODULE macro

Creating Your First Module

1
Create the Module Header
2
Create a new Objective-C header file:
3
Objective-C
// CalendarModule.h
#import <React/RCTBridgeModule.h>

@interface CalendarModule : NSObject <RCTBridgeModule>
@end
Swift
For Swift modules, create a bridging header and the Swift file:
// CalendarModule-Bridging-Header.h
#import <React/RCTBridgeModule.h>
4
Create the Module Implementation
5
Objective-C
// CalendarModule.m
#import "CalendarModule.h"
#import <React/RCTLog.h>

@implementation CalendarModule

// Export module to JavaScript
RCT_EXPORT_MODULE();

// Export method to JavaScript
RCT_EXPORT_METHOD(createEvent:(NSString *)name
                  location:(NSString *)location
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  NSString *eventId = [self createCalendarEvent:name location:location];
  
  if (eventId) {
    resolve(eventId);
  } else {
    NSError *error = [NSError errorWithDomain:@"CalendarModule" 
                                         code:1 
                                     userInfo:nil];
    reject(@"CREATE_EVENT_ERROR", @"Failed to create event", error);
  }
}

- (NSString *)createCalendarEvent:(NSString *)name location:(NSString *)location
{
  // Your native implementation
  return @"event-123";
}

@end
Swift
// CalendarModule.swift
import Foundation

@objc(CalendarModule)
class CalendarModule: NSObject {
  
  @objc
  func createEvent(
    _ name: String,
    location: String,
    resolver resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    let eventId = createCalendarEvent(name: name, location: location)
    
    if let eventId = eventId {
      resolve(eventId)
    } else {
      let error = NSError(domain: "CalendarModule", code: 1, userInfo: nil)
      reject("CREATE_EVENT_ERROR", "Failed to create event", error)
    }
  }
  
  private func createCalendarEvent(name: String, location: String) -> String? {
    // Your native implementation
    return "event-123"
  }
}
Create the Objective-C bridge file:
// CalendarModule.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(CalendarModule, NSObject)

RCT_EXTERN_METHOD(createEvent:(NSString *)name
                  location:(NSString *)location
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

@end
6
Use from JavaScript
7
import { NativeModules } from 'react-native';

const { CalendarModule } = NativeModules;

// Call the native method
try {
  const eventId = await CalendarModule.createEvent(
    'Team Meeting',
    'Conference Room A'
  );
  console.log('Created event:', eventId);
} catch (error) {
  console.error('Error creating event:', error);
}

Exporting Methods

Basic Method Export

Use the RCT_EXPORT_METHOD macro to expose methods to JavaScript:
RCT_EXPORT_METHOD(methodName:(NSString *)param)
{
  // Implementation
}

Method Parameters

Supported parameter types:
JavaScript TypeObjective-C TypeSwift Type
booleanBOOLBool
numberNSInteger, CGFloat, doubleInt, Double, Float
stringNSStringString
arrayNSArray[Any]
objectNSDictionary[String: Any]
functionRCTResponseSenderBlockClosure

Custom Method Names

Use RCT_REMAP_METHOD to specify a different JavaScript name:
RCT_REMAP_METHOD(executeQuery,
                 query:(NSString *)query
                 parameters:(NSDictionary *)parameters
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject)
{
  // Implementation
}

Using Callbacks

Single Callback

RCT_EXPORT_METHOD(fetchData:(RCTResponseSenderBlock)callback)
{
  NSString *result = @"data";
  callback(@[result]);
}
From JavaScript:
CalendarModule.fetchData((result) => {
  console.log(result);
});

Error and Success Callbacks

RCT_EXPORT_METHOD(fetchData:(RCTResponseSenderBlock)callback)
{
  NSError *error = nil;
  NSString *result = [self getData:&error];
  
  if (error) {
    callback(@[error.localizedDescription, [NSNull null]]);
  } else {
    callback(@[[NSNull null], result]);
  }
}

Using Promises

Promises are the preferred way to handle async operations:
RCT_EXPORT_METHOD(getEventById:(NSString *)eventId
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  NSDictionary *event = [self findEvent:eventId];
  
  if (event) {
    resolve(event);
  } else {
    NSError *error = [NSError errorWithDomain:@"CalendarModule"
                                         code:404
                                     userInfo:@{NSLocalizedDescriptionKey: @"Event not found"}];
    reject(@"EVENT_NOT_FOUND", @"Event not found", error);
  }
}
From JavaScript:
try {
  const event = await CalendarModule.getEventById('123');
  console.log(event);
} catch (error) {
  console.error(error);
}

Exporting Constants

Export constants that are available synchronously in JavaScript:
- (NSDictionary *)constantsToExport
{
  return @{
    @"PLATFORM": @"ios",
    @"VERSION": [[UIDevice currentDevice] systemVersion],
    @"MODEL": [[UIDevice currentDevice] model]
  };
}
From JavaScript:
const { PLATFORM, VERSION, MODEL } = CalendarModule.getConstants();
console.log(`Running on ${PLATFORM}, ${VERSION}, ${MODEL}`);

Module Initialization

Controlling Initialization

+ (BOOL)requiresMainQueueSetup
{
  return YES; // Initialize on main thread
}

Accessing React Bridge

@interface CalendarModule : NSObject <RCTBridgeModule>
@end

@implementation CalendarModule

@synthesize bridge = _bridge;

RCT_EXPORT_MODULE();

- (void)someMethod
{
  // Access the bridge
  if (self.bridge) {
    // Use bridge
  }
}

@end

Threading

Running on Main Queue

By default, methods run on a background queue. To run on the main queue:
RCT_EXPORT_METHOD(updateUI)
{
  dispatch_async(dispatch_get_main_queue(), ^{
    // UI updates here
  });
}

Custom Method Queue

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

Swift Modules

Complete Swift Example

// DeviceInfo.swift
import Foundation
import UIKit

@objc(DeviceInfo)
class DeviceInfo: NSObject {
  
  @objc
  func getBatteryLevel(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    UIDevice.current.isBatteryMonitoringEnabled = true
    let batteryLevel = UIDevice.current.batteryLevel
    
    if batteryLevel >= 0 {
      resolve(batteryLevel)
    } else {
      reject("BATTERY_ERROR", "Could not get battery level", nil)
    }
  }
  
  @objc
  static func requiresMainQueueSetup() -> Bool {
    return true
  }
}

Example: Alert Manager Module

Here’s a real-world example based on React Native’s source code (packages/react-native/React/CoreModules/RCTAlertManager.mm:43-48):
@implementation RCTAlertManager
{
  NSHashTable *_alertControllers;
}

RCT_EXPORT_MODULE()

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

- (void)invalidate
{
  RCTExecuteOnMainQueue(^{
    for (UIAlertController *alertController in self->_alertControllers) {
      [alertController.presentingViewController 
          dismissViewControllerAnimated:YES 
                             completion:nil];
    }
  });
}

RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
                  callback:(RCTResponseSenderBlock)callback)
{
  NSString *title = args[@"title"];
  NSString *message = args[@"message"];
  NSArray<NSDictionary *> *buttons = args[@"buttons"];
  
  RCTExecuteOnMainQueue(^{
    UIAlertController *alertController = 
        [UIAlertController alertControllerWithTitle:title
                                            message:message
                                     preferredStyle:UIAlertControllerStyleAlert];
    
    for (NSDictionary *button in buttons) {
      NSString *buttonKey = button.allKeys.firstObject;
      NSString *buttonTitle = button[buttonKey];
      
      UIAlertAction *action = 
          [UIAlertAction actionWithTitle:buttonTitle
                                   style:UIAlertActionStyleDefault
                                 handler:^(UIAlertAction *action) {
                                   callback(@[buttonKey]);
                                 }];
      
      [alertController addAction:action];
    }
    
    if (self->_alertControllers == nil) {
      self->_alertControllers = [NSHashTable weakObjectsHashTable];
    }
    [self->_alertControllers addObject:alertController];
    
    UIViewController *rootVC = 
        [UIApplication sharedApplication].delegate.window.rootViewController;
    [rootVC presentViewController:alertController 
                         animated:YES 
                       completion:nil];
  });
}

@end

Lifecycle Methods

Module Cleanup

- (void)invalidate
{
  // Clean up resources
  // Cancel ongoing operations
  // Remove observers
}

Batch Completion

- (void)batchDidComplete
{
  // Called after a batch of JS method invocations
}

Accessing iOS APIs

Getting Current View Controller

RCT_EXPORT_METHOD(presentModal)
{
  dispatch_async(dispatch_get_main_queue(), ^{
    UIViewController *rootVC = RCTPresentedViewController();
    // Present your modal
  });
}

Working with UIKit

RCT_EXPORT_METHOD(showToast:(NSString *)message)
{
  dispatch_async(dispatch_get_main_queue(), ^{
    UIAlertController *alert = 
        [UIAlertController alertControllerWithTitle:nil
                                            message:message
                                     preferredStyle:UIAlertControllerStyleAlert];
    
    UIViewController *rootVC = RCTPresentedViewController();
    [rootVC presentViewController:alert animated:YES completion:^{
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC),
                     dispatch_get_main_queue(), ^{
        [alert dismissViewControllerAnimated:YES completion:nil];
      });
    }];
  });
}

Best Practices

  • Always handle errors and reject promises appropriately
  • Use meaningful error codes and messages
  • Run UI operations on the main queue
  • Clean up resources in invalidate
  • Use promises for async operations
  • Document your module’s API
  • Mark Swift classes with @objc for React Native compatibility
  • Use requiresMainQueueSetup to control initialization threading
  • Validate input parameters before processing

Build docs developers (and LLMs) love