Native Module is a Java or Objective-C class that makes native platform APIs accessible from JavaScript in React Native. Each module registers in the Bridge and exports methods that can be called from JS code like regular functions. According to Meta, 2024, Native Module remains the primary way to integrate platform code into React Native applications.
Key Takeaways
Native Module is an architectural element of React Native that allows executing code on the platform language (Objective-C/Swift for iOS, Java/Kotlin for Android) and returning the result to JavaScript. Without a Native Module it is impossible to access native device capabilities — camera, GPS, accelerometer, file system or Bluetooth.
React Native ships with a set of built-in Native Modules: CameraRoll, AsyncStorage, Geolocation, NetInfo and others. However, for specific tasks — integrating third-party SDKs, working with hardware sensors or background processes — the developer creates custom modules. According to the State of React Native 2024 survey, 67% of developers use at least one custom Native Module in their projects.
The Native Module architecture depends on the React Native version. In the classic architecture (React Native 0.72 and older) the module connects through the Bridge and communicates with JS asynchronously via JSON serialization. In the new architecture (React Native 0.76+) the module can work as a Turbo Module, using JSI for synchronous access without serialization.
Creating a Native Module for iOS starts with declaring an Objective-C class that implements the RCTBridgeModule protocol. The RCT_EXPORT_MODULE macro registers the module in the Bridge, and RCT_EXPORT_METHOD exports a method available from JavaScript.
// ImageCompressor.m — Native Module for iOS
@interface ImageCompressor () RCT_EXPORT_MODULE()
@end
@implementation ImageCompressor
RCT_EXPORT_METHOD(compressImage:(NSString *)imagePath
quality:(NSNumber *)quality
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
NSData *compressedData = [UIImageJPEGRepresentation(image, quality.floatValue)];
NSString *outputPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"compressed.jpg"];
[compressedData writeToFile:outputPath atomically:YES];
resolve(outputPath);
}
@end
The compressImage method takes an image path and compression quality (0.0–1.0), processes the data on the native side and returns the path to the compressed file. The key advantage is that compression is performed by native code, which is significantly faster and more memory-efficient than the equivalent JavaScript operation.
For Swift modules, the @objc annotation is used before the class and methods to make them available to the Objective-C runtime that the Bridge works with. The class must inherit NSObject and implement RCTBridgeModule.
// ImageCompressor.swift — Swift Native Module
@objc(ImageCompressor)
class ImageCompressor: NSObject {
@objc
func compressImage(
_ imagePath: String,
quality: Float,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
guard let image = UIImage(contentsOfFile: imagePath) else {
rejecter("FILE_ERROR", "Cannot load image", nil)
return
}
guard let data = image.jpegData(compressionQuality: CGFloat(quality)) else {
rejecter("COMPRESS_ERROR", "Compression failed", nil)
return
}
let outputPath = NSTemporaryDirectory() + "compressed.jpg"
try? data.write(to: URL(fileURLWithPath: outputPath))
resolver(outputPath)
}
}
On Android, a Native Module is created as a Java class extending ReactContextBaseJavaModule. The @ReactMethod annotation exports the method to the Bridge. The Promise interface from com.facebook.react.bridge is used to return results.
// ImageCompressorModule.java — Android Native Module
public class ImageCompressorModule
extends ReactContextBaseJavaModule {
@Override
public String getName() {
return "ImageCompressor";
}
@ReactMethod
public void compressImage(
String imagePath,
Float quality,
Promise promise) {
try {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
File outputFile = new File(
ReactNative.getApplicationContext()
.getCacheDir(), "compressed.jpg");
FileOutputStream fos = new FileOutputStream(outputFile);
bitmap.compress(
Bitmap.CompressFormat.JPEG,
(int)(quality * 100), fos);
fos.close();
promise.resolve(outputFile.getAbsolutePath());
} catch (Exception e) {
promise.reject("COMPRESS_ERROR", e.getMessage());
}
}
}
The getName() method returns the module name under which it will be accessible from JavaScript. In the example above, the module is registered as ImageCompressor. The @ReactMethod annotation tells the Bridge that the method should be exported. Important: methods must be void and only accept types supported by the Bridge: String, Boolean, Integer, Double, ReadableArray, ReadableMap, Promise.
After creating the module class, it must be registered in the application package. For this, a class implementing ReactPackage is created and added to the module list in the createNativeModules method.
// ImageCompressorPackage.java — module registration
public class ImageCompressorPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
return Arrays.asList(
new ImageCompressorModule(reactContext)
);
}
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
After creating modules for both platforms, they need to be registered in React Native. For Android, the package is added in MainApplication.java in the getPackages() method. For iOS, the module is registered automatically via the RCT_EXPORT_MODULE macro, but manual registration in AppDelegate.mm can also be used.
// MainApplication.java — adding package to React Native
import com.yourapp.nativemodules.ImageCompressorPackage;
public class MainApplication extends Application
implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages =
new PackageList(this).getPackages();
packages.add(new ImageCompressorPackage());
return packages;
}
};
}
After registration, the module becomes available in JavaScript through NativeModules. React Native automatically uses the module name specified in getName() for Android or RCT_EXPORT_MODULE for iOS.
// Using Native Module from JavaScript
import { NativeModules } from 'react-native';
import { Platform } from 'react-native';
const ImageCompressor = NativeModules.ImageCompressor;
async function compressPhoto(uri: string) {
try {
const result = await ImageCompressor.compressImage(
uri.replace('file://', ''), 0.8
);
console.log('Compressed:', result);
return result;
} catch (error) {
console.error('Compression failed:', error);
throw error;
}
}
Comparing the classic Native Module with Turbo Module helps understand which approach to choose for a new project. Both mechanisms provide access to native code, but differ fundamentally in architecture and performance.
| Characteristic | Native Module (Bridge) | Turbo Module (JSI) |
|---|---|---|
| Communication | Asynchronous via JSON | Synchronous via JSI |
| Serialization | JSON on every call | No data copying |
| Typing | Manual, no generation | Auto via Codegen |
| Loading | On application initialization | Lazy load |
| Compatibility | All React Native versions | React Native 0.73+ |
For existing projects on React Native 0.72 and older, classic Native Modules remain the primary choice. For new projects, Turbo Module is recommended, especially if high performance is required for frequent native method calls. With the gradual update of React Native, the community is moving toward a full transition to the new architecture.
Frequently Asked Questions
Yes, the Bridge supports callbacks. Instead of a Promise, you can use RCTResponseSenderBlock callback functions in iOS and Callback in Android. However, Promise is considered the modern standard and is recommended for new modules.
A Native Module is debugged like regular native code — set breakpoints in Xcode or Android Studio. For iOS, use the build scheme with React Native; for Android, use the Debug configuration. The entry point is the methods called from JS.
Native Module via the Bridge does not support binary data (NSData/byte[]), custom objects and functions. For images, use a file path or base64 string. Turbo Module via JSI removes some of these limitations.
Usually no — the Native Module API is stable and backward compatible. When transitioning to the new architecture (Turbo Module), the module is adapted through decorators, but existing code continues to work.
Use RCTEventEmitter in iOS or DeviceEventEmitter in Android. The module sends an event, and the JS side subscribes through NativeEventEmitter from react-native. This is useful for streaming data and sensor events.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also