Create custom React Native native modules for iOS and Android. Use when integrating native SDKs, optimizing performance-critical code, or accessing platform-specific APIs...
Build custom native modules to bridge JavaScript and native code in React Native.
Native modules expose native functionality to JavaScript. Choose based on React Native version:
Design JavaScript API:
// What you want to call from JS
import { NativeModules } from 'react-native';
const { MyModule } = NativeModules;
// Synchronous
const result = MyModule.getValue();
// Asynchronous (Promise)
const data = await MyModule.fetchData();
// With callback
MyModule.processData(input, (error, result) => {
if (error) console.error(error);
else console.log(result);
});
// Event emitter
MyModule.addListener('onUpdate', (event) => {
console.log(event);
});
Keep bridge calls minimal:
File structure:
MyModule/
āāā ios/
ā āāā MyModule.h
ā āāā MyModule.m (or .swift)
ā āāā MyModule-Bridging-Header.h (if Swift)
āāā android/
ā āāā src/main/java/com/mymodule/
ā āāā MyModulePackage.java
ā āāā MyModule.java (or .kt)
āāā js/
ā āāā NativeMyModule.ts
āāā package.json
Objective-C (.h file):
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface MyModule : RCTEventEmitter <RCTBridgeModule>
@end
Objective-C (.m file):
#import "MyModule.h"
@implementation MyModule
RCT_EXPORT_MODULE();
// Synchronous method
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getValue)
{
return @"value";
}
// Async with Promise
RCT_EXPORT_METHOD(fetchData:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
// Perform operation
if (success) {
resolve(@{@"data": result});
} else {
reject(@"ERROR_CODE", @"Error message", error);
}
}
// Async with callback
RCT_EXPORT_METHOD(processData:(NSString *)input
callback:(RCTResponseSenderBlock)callback)
{
// Process data
callback(@[[NSNull null], result]); // [error, result]
}
// Event emitter
- (NSArray<NSString *> *)supportedEvents
{
return @[@"onUpdate"];
}
- (void)sendUpdate:(NSDictionary *)data
{
[self sendEventWithName:@"onUpdate" body:data];
}
@end
Java module:
package com.mymodule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class MyModule extends ReactContextBaseJavaModule {
private ReactApplicationContext reactContext;
public MyModule(ReactApplicationContext context) {
super(context);
this.reactContext = context;
}
@Override
public String getName() {
return "MyModule";
}
// Synchronous method
@ReactMethod(isBlockingSynchronousMethod = true)
public String getValue() {
return "value";
}
// Async with Promise
@ReactMethod
public void fetchData(Promise promise) {
try {
WritableMap result = Arguments.createMap();
result.putString("data", "value");
promise.resolve(result);
} catch (Exception e) {
promise.reject("ERROR_CODE", "Error message", e);
}
}
// Async with callback
@ReactMethod
public void processData(String input, Callback callback) {
try {
String result = process(input);
callback.invoke(null, result); // error, result
} catch (Exception e) {
callback.invoke(e.getMessage(), null);
}
}
// Event emitter
private void sendEvent(String eventName, WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
Package registration:
package com.mymodule;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MyModulePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MyModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Auto-linking (RN >= 0.60):
Create package.json in module root:
{
"name": "react-native-my-module",
"version": "1.0.0",
"main": "js/index.js",
"react-native": "js/index.js"
}
Manual linking (if needed):
iOS: Add to Podfile
pod 'MyModule', :path => '../node_modules/react-native-my-module'
Android: Add to settings.gradle
include ':react-native-my-module'
project(':react-native-my-module').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-my-module/android')
// js/NativeMyModule.ts
import { NativeModules, NativeEventEmitter } from 'react-native';
interface MyModuleInterface {
getValue(): string;
fetchData(): Promise<{ data: string }>;
processData(input: string, callback: (error: string | null, result: string | null) => void): void;
addListener(eventName: string, listener: (event: any) => void): void;
removeListeners(count: number): void;
}
const { MyModule } = NativeModules;
const eventEmitter = new NativeEventEmitter(MyModule);
export default MyModule as MyModuleInterface;
export { eventEmitter };
iOS (run on background thread):
RCT_EXPORT_METHOD(heavyTask:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Heavy operation
NSString *result = [self performHeavyOperation];
dispatch_async(dispatch_get_main_queue(), ^{
resolve(result);
});
});
}
Android (run on background thread):
@ReactMethod
public void heavyTask(Promise promise) {
new Thread(() -> {
try {
String result = performHeavyOperation();
promise.resolve(result);
} catch (Exception e) {
promise.reject("ERROR", e);
}
}).start();
}
iOS:
// JS -> Native
NSString *string = [RCTConvert NSString:value];
NSNumber *number = [RCTConvert NSNumber:value];
NSArray *array = [RCTConvert NSArray:value];
NSDictionary *dict = [RCTConvert NSDictionary:value];
// Native -> JS
return @{
@"string": @"value",
@"number": @(42),
@"array": @[@"a", @"b"],
@"dict": @{@"key": @"value"}
};
Android:
// JS -> Native (automatic)
String string = input;
int number = input;
ReadableArray array = input;
ReadableMap map = input;
// Native -> JS
WritableMap result = Arguments.createMap();
result.putString("string", "value");
result.putInt("number", 42);
WritableArray array = Arguments.createArray();
array.pushString("a");
array.pushString("b");
result.putArray("array", array);
iOS:
RCT_EXPORT_METHOD(riskyOperation:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSError *error = nil;
id result = [self performOperation:&error];
if (error) {
reject(@"OPERATION_FAILED", error.localizedDescription, error);
} else {
resolve(result);
}
}
Android:
@ReactMethod
public void riskyOperation(Promise promise) {
try {
Object result = performOperation();
promise.resolve(result);
} catch (Exception e) {
promise.reject("OPERATION_FAILED", e.getMessage(), e);
}
}
For detailed platform-specific guides:
Module not found:
pod install (iOS) or rebuild (Android)Methods not available:
Crashes on method call:
Events not received: