Documentation Index
Fetch the complete documentation index at: https://mintlify.com/facebook/react-native/llms.txt
Use this file to discover all available pages before exploring further.
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:
- A class implementing the
RCTBridgeModule protocol
- Methods exported with
RCT_EXPORT_METHOD macro
- Module registration with
RCT_EXPORT_MODULE macro
Creating Your First Module
Create a new Objective-C header file:
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>
Create the Module Implementation
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
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 Type | Objective-C Type | Swift Type |
|---|
boolean | BOOL | Bool |
number | NSInteger, CGFloat, double | Int, Double, Float |
string | NSString | String |
array | NSArray | [Any] |
object | NSDictionary | [String: Any] |
function | RCTResponseSenderBlock | Closure |
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
Swift Module
Objective-C Bridge
// 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
}
}
// DeviceInfo.m
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(DeviceInfo, NSObject)
RCT_EXTERN_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
@end
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