Creating a React Native Library with Native Modules
React Native Community offers hundreds of ready-made libraries, but some tasks—proprietary vendor SDK, non-standard camera or biometric work, MDM-system integration—require writing a native module from scratch. Since New Architecture (JSI + TurboModules) appeared, the old @ReactMethod guides are outdated.
Old Architecture vs New Architecture: Fundamental Difference
Old Bridge architecture works asynchronously via JSON serialization. Native method call: JavaScript → JSON serialization → Bridge queue → deserialization → Java/ObjC. This adds ~1–5 ms per call and makes synchronous native access impossible.
New Architecture uses JSI (JavaScript Interface)—direct C++ binding between JS engine (Hermes) and native code. TurboModules load lazily and call synchronously. For high-frequency operations (every frame of animation, real-time audio processing), this is critical.
React Native 0.73+ includes New Architecture by default. Libraries must support both via codegen specification.
Creating a Library via create-react-native-library
npx create-react-native-library@latest my-module is the standard scaffold. Generates:
my-module/
android/src/main/java/…/MyModule.kt
ios/MyModule.mm (Objective-C++ for JSI bridge)
src/index.tsx — TypeScript API
src/NativeMyModule.ts — codegen spec
Codegen Specification
A TypeScript file describing the contract, from which codegen generates C++ glue code:
// NativeMyModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
multiply(a: number, b: number): Promise<number>;
getDeviceId(): string; // synchronous—only in New Architecture
}
export default TurboModuleRegistry.getEnforcing<Spec>('MyModule');
getEnforcing throws at startup if native module isn't registered—better than silent undefined.
Android Implementation: Kotlin + ReactPackage
class MyModule(reactContext: ReactApplicationContext) :
NativeMyModuleSpec(reactContext) {
override fun getName() = NAME
override fun multiply(a: Double, b: Double): Promise<Double> {
return Promise.resolve(a * b)
}
override fun getDeviceId(): String {
return Settings.Secure.getString(
reactApplicationContext.contentResolver,
Settings.Secure.ANDROID_ID
)
}
companion object {
const val NAME = "MyModule"
}
}
NativeMyModuleSpec is an abstract class generated by codegen from TypeScript spec. Unimplemented method—compilation error, not runtime crash. This is New Architecture's key advantage.
ReactPackage registers the module:
class MyPackage : ReactPackage {
override fun createNativeModules(context: ReactApplicationContext) =
listOf(MyModule(context))
override fun createViewManagers(context: ReactApplicationContext) = emptyList<ViewManager<*, *>>()
}
iOS: Objective-C++ Bridge
For New Architecture, iOS implementation uses Objective-C++ (.mm) or Swift with ObjC wrapper. Swift doesn't natively support JSI without a bridge, so .mm with #import <React/RCTUtils.h> remains mandatory.
// MyModule.mm
#import "MyModule.h"
#import <React/RCTUtils.h>
@implementation MyModule
RCT_EXPORT_MODULE()
- (NSString *)getDeviceId {
return [[[UIDevice currentDevice] identifierForVendor] UUIDString];
}
@end
For synchronous methods in Old Architecture: RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD works but blocks JS thread. In New Architecture, synchronicity via JSI doesn't block JS—fundamentally different model.
Native View Components
For custom native View (e.g., maps SDK, custom video player), use ViewManager on Android / RCTViewManager on iOS. New Architecture introduces Fabric for native components—analog of TurboModules for views. Codegen generates ComponentDescriptor from TypeScript spec with codegenNativeComponent.
Expo Support
If your app uses Expo managed workflow—native modules require Expo Modules API instead of raw React Native. npx create-expo-module generates correct scaffold. ExpoModule registers automatically without ReactPackage—Expo Autolinking finds it via package.json.
Typical Errors
-
Module not found at runtime—forgot
pod installon iOS after adding module -
Mismatched types—TypeScript spec says
number, Kotlin takesDouble(ok), Swift takesInt(crash). All JS numbers areDoubleon native side -
Main thread violation—calling UI code from native method without dispatch to main thread:
DispatchQueue.main.async/UiThreadUtil.runOnUiThread
Native module development: simple operations (1–3 methods)—3–5 days. Complex with EventEmitter, View-components, Old and New Architecture support—3–5 weeks. Cost calculated individually.







