TurboModules are React Native’s next-generation native module system, replacing the legacy NativeModules. Built on JSI, TurboModules provide lazy loading, type safety through Codegen, and direct synchronous communication between JavaScript and native code.
Why TurboModules?
The legacy native module system had several limitations:
Eager initialization - All modules loaded at startup, even if never used
Bridge overhead - JSON serialization for every call
No type safety - Type mismatches only caught at runtime
Async-only - All calls asynchronous, even for simple getters
Memory waste - Unused modules consume memory
TurboModules solve these problems:
Lazy loading - Modules initialized only when first accessed
JSI direct calls - No serialization overhead
Codegen type safety - Compile-time type checking
Synchronous calls - Support both sync and async methods
Efficient - Smaller memory footprint and faster startup
Architecture
TurboModule Base Class
All TurboModules extend the base class defined in ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h:
class JSI_EXPORT TurboModule : public jsi :: HostObject {
public:
TurboModule ( std :: string name , std :: shared_ptr < CallInvoker > jsInvoker );
// Called when JavaScript accesses a property
jsi :: Value get ( jsi :: Runtime & runtime , const jsi :: PropNameID & propName ) override ;
// Returns list of available methods
std :: vector < jsi :: PropNameID > getPropertyNames ( jsi :: Runtime & runtime ) override ;
protected:
const std ::string name_;
std ::shared_ptr < CallInvoker > jsInvoker_;
std ::unordered_map < std ::string, MethodMetadata > methodMap_;
};
Key concepts:
HostObject - TurboModules are JSI HostObjects, accessible from JavaScript
Method metadata - Each method has metadata (arg count, invoker function)
Call invoker - Schedules work on appropriate threads
Lazy method binding - Methods bound to JavaScript objects on first access
Method Value Kinds
TurboModule methods can return different types:
enum TurboModuleMethodValueKind {
VoidKind , // void return
BooleanKind , // bool return
NumberKind , // double/float/int return
StringKind , // std::string return
ObjectKind , // jsi::Object return
ArrayKind , // jsi::Array return
FunctionKind , // jsi::Function (callback)
PromiseKind , // Promise return (async)
};
Codegen automatically determines the correct kind based on your TypeScript/Flow spec.
Creating a TurboModule
1. Define JavaScript Spec
Create a spec file using TypeScript or Flow:
// NativeMyModule.ts
import { TurboModule , TurboModuleRegistry } from 'react-native' ;
export interface Spec extends TurboModule {
// Synchronous method
getString () : string ;
// Async method returning Promise
fetchData ( url : string ) : Promise <{ data : string }>;
// Method with callback
processData (
data : string ,
callback : ( result : string ) => void
) : void ;
// Method with multiple args and types
calculate (
x : number ,
y : number ,
operation : 'add' | 'subtract'
) : number ;
// Get constants object
getConstants () : {
API_URL : string ;
MAX_RETRIES : number ;
};
}
export default TurboModuleRegistry . getEnforcing < Spec >( 'MyModule' ) ;
2. Run Codegen
Codegen generates the native interfaces:
# Automatically runs during build
yarn react-native codegen
Outputs:
MyModuleSpec.h - C++ abstract base class
iOS: MyModuleSpec-generated.mm - Objective-C++ glue code
Android: MyModuleSpec.java - Java/Kotlin interfaces
3. Implement Native Code
iOS Implementation (Objective-C++)
// RCTMyModule.h
#import <MyModuleSpec/MyModuleSpec.h>
#import <React/RCTBridgeModule.h>
@interface RCTMyModule : NSObject <NativeMyModuleSpec>
@end
// RCTMyModule.mm
#import "RCTMyModule.h"
@implementation RCTMyModule
RCT_EXPORT_MODULE (MyModule)
- (std::shared_ptr<facebook::react::TurboModule>) getTurboModule :
( const facebook::react::ObjCTurboModule::InitParams & ) params {
return std::make_shared < facebook::react::NativeMyModuleSpecJSI > (params);
}
// Implement spec methods
- ( NSString * ) getString {
return @"Hello from native!" ;
}
- ( void ) fetchData: ( NSString * ) url
resolve: (RCTPromiseResolveBlock) resolve
reject: (RCTPromiseRejectBlock) reject {
// Async work
dispatch_async ( dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^ {
NSString * data = [ self performNetworkRequest: url];
resolve (@{ @"data" : data});
});
}
- ( void ) processData: ( NSString * ) data
callback: (RCTResponseSenderBlock) callback {
NSString * result = [ self process: data];
callback (@[result]);
}
- ( double ) calculate: ( double ) x
y: ( double ) y
operation: ( NSString * ) operation {
if ([operation isEqualToString: @"add" ]) {
return x + y;
} else {
return x - y;
}
}
- (facebook::react::ModuleConstants<JS::NativeMyModule::Constants>) getConstants {
return facebook::react::typedConstants < JS::NativeMyModule::Constants > ({
.API_URL = "https://api.example.com" ,
.MAX_RETRIES = 3 ,
});
}
@end
Android Implementation (Kotlin)
// MyModule.kt
package com.myapp
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Callback
import com.facebook.react.module.annotations.ReactModule
import com.myapp.NativeMyModuleSpec
@ReactModule (name = "MyModule" )
class MyModule (reactContext: ReactApplicationContext ) :
NativeMyModuleSpec (reactContext) {
override fun getName () = "MyModule"
@ReactMethod (isBlockingSynchronousMethod = true )
override fun getString (): String {
return "Hello from native!"
}
@ReactMethod
override fun fetchData (url: String , promise: Promise ) {
// Async work
Thread {
try {
val data = performNetworkRequest (url)
val result = Arguments. createMap (). apply {
putString ( "data" , data )
}
promise. resolve (result)
} catch (e: Exception ) {
promise. reject ( "ERROR" , e.message, e)
}
}. start ()
}
@ReactMethod
override fun processData ( data : String , callback: Callback ) {
val result = process ( data )
callback. invoke (result)
}
@ReactMethod (isBlockingSynchronousMethod = true )
override fun calculate (x: Double , y: Double , operation: String ): Double {
return when (operation) {
"add" -> x + y
"subtract" -> x - y
else -> 0.0
}
}
override fun getTypedExportedConstants (): Map < String , Any > {
return mapOf (
"API_URL" to "https://api.example.com" ,
"MAX_RETRIES" to 3
)
}
}
4. Register Module
iOS
Add to your TurboModule provider:
// RCTAppDelegate.mm
- ( Class )getModuleClassFromName:( const char * )name {
if ( std::strcmp (name, "MyModule" ) == 0 ) {
return RCTMyModule . class ;
}
return [ super getModuleClassFromName: name];
}
Android
Add to package:
// MyPackage.kt
class MyPackage : TurboReactPackage () {
override fun getModule (
name: String ,
reactContext: ReactApplicationContext
): NativeModule ? {
return when (name) {
"MyModule" -> MyModule (reactContext)
else -> null
}
}
override fun getReactModuleInfoProvider (): ReactModuleInfoProvider {
return ReactModuleInfoProvider {
mapOf (
"MyModule" to ReactModuleInfo (
"MyModule" ,
"MyModule" ,
false , // canOverrideExistingModule
false , // needsEagerInit
true , // isCxxModule
true // isTurboModule
)
)
}
}
}
5. Use in JavaScript
import NativeMyModule from './NativeMyModule' ;
// Synchronous call
const greeting = NativeMyModule . getString ();
console . log ( greeting ); // "Hello from native!"
// Async call
const { data } = await NativeMyModule . fetchData ( 'https://api.example.com' );
// Callback
NativeMyModule . processData ( 'input' , ( result ) => {
console . log ( 'Processed:' , result );
});
// Calculation
const sum = NativeMyModule . calculate ( 5 , 3 , 'add' ); // 8
// Constants
const { API_URL , MAX_RETRIES } = NativeMyModule . getConstants ();
Lazy Loading
TurboModules are initialized lazily:
// Module NOT loaded yet
import NativeMyModule from './NativeMyModule' ;
// Still not loaded...
// NOW loaded - first access triggers initialization
const value = NativeMyModule . getString ();
// Subsequent calls use already-initialized module
const value2 = NativeMyModule . getString ();
Benefits:
Faster startup - Only frequently-used modules loaded
Lower memory - Unused modules never allocated
On-demand - Modules loaded exactly when needed
Eager Initialization
For modules that must be loaded at startup:
// Android
ReactModuleInfo (
name = "MyModule" ,
className = "MyModule" ,
canOverrideExistingModule = false ,
needsEagerInit = true , // Force early init
isCxxModule = true ,
isTurboModule = true
)
// iOS
+ ( BOOL )requiresMainQueueSetup {
return YES ; // Init on main queue at startup
}
Synchronous vs Asynchronous Methods
Synchronous Methods
Return values directly:
// Spec
getValue (): string ;
// iOS
- ( NSString * ) getValue {
return @ "value" ;
}
// Android - note the annotation!
@ ReactMethod ( isBlockingSynchronousMethod = true )
override fun getValue (): String {
return "value"
}
// JavaScript - returns immediately
const value = NativeModule . getValue ();
Synchronous methods block the JavaScript thread. Keep them fast (less than 1ms) or users will experience jank.
Asynchronous Methods
Return promises:
// Spec
fetchData ( url : string ): Promise < Data > ;
// iOS
- ( void ) fetchData :( NSString * ) url
resolve :( RCTPromiseResolveBlock ) resolve
reject :( RCTPromiseRejectBlock ) reject {
// Do async work, then:
resolve ( result );
// or on error:
reject (@ "ERROR_CODE" , @ "Error message" , error );
}
// Android
@ ReactMethod
override fun fetchData ( url : String , promise : Promise ) {
// Do async work, then:
promise . resolve ( result )
// or on error:
promise . reject ( "ERROR_CODE" , "Error message" , error )
}
// JavaScript - use await or .then()
const data = await NativeModule . fetchData ( url );
CallInvoker and Threading
The CallInvoker schedules work on appropriate threads:
class CallInvoker {
public:
virtual void invokeAsync ( std :: function < void ()> && func ) = 0 ;
virtual void invokeSync ( std :: function < void ()> && func ) = 0 ;
};
Scheduling JavaScript Callbacks
// From C++, schedule work on JavaScript thread
jsInvoker_ -> invokeAsync ([ = ]( jsi :: Runtime & rt ) {
// This runs on JavaScript thread
jsi ::Function callback = callbackValue . asObject (rt). asFunction (rt);
callback . call (rt, result);
});
Thread Requirements
Specify where methods should execute:
// iOS - require main queue
- ( void )updateUI {
dispatch_assert_queue ( dispatch_get_main_queue ());
// Update UI
}
// Android - require UI thread
@ReactMethod
fun updateUI () {
require (UiThreadUtil. isOnUiThread ())
// Update UI
}
Type Safety with Codegen
Codegen ensures type safety across the boundary:
Primitive Types
interface Spec extends TurboModule {
process (
str : string ,
num : number ,
bool : boolean ,
// null/undefined handled automatically
optional ?: string
) : void ;
}
Complex Types
interface Spec extends TurboModule {
// Objects
getConfig () : {
apiKey : string ;
timeout : number ;
};
// Arrays
getItems () : Array <{ id : string ; name : string }>;
// Unions (limited support)
getValue () : string | number ;
// Callbacks
subscribe ( callback : ( event : { type : string }) => void ) : void ;
}
Enums
enum Status {
IDLE = 'idle' ,
LOADING = 'loading' ,
SUCCESS = 'success' ,
ERROR = 'error' ,
}
interface Spec extends TurboModule {
getStatus () : Status ;
}
Codegen converts to native enums (iOS) or constants (Android).
Constants and Configuration
Modules can export constants:
interface Spec extends TurboModule {
getConstants () : {
VERSION : string ;
API_ENDPOINT : string ;
MAX_ITEMS : number ;
};
}
Constants are:
Evaluated once - At module initialization
Cached - Same object returned for all calls
Synchronous - No async access needed
Event Emission
TurboModules can emit events to JavaScript:
// C++ TurboModule
class MyTurboModule : public TurboModule {
public:
void emitEvent ( std :: string eventName , jsi :: Object data ) {
emitDeviceEvent (runtime, eventName, std :: move (data));
}
};
// JavaScript - subscribe to events
import { NativeEventEmitter } from 'react-native' ;
import NativeMyModule from './NativeMyModule' ;
const eventEmitter = new NativeEventEmitter ( NativeMyModule );
const subscription = eventEmitter . addListener ( 'onData' , ( data ) => {
console . log ( 'Received:' , data );
});
// Clean up
subscription . remove ();
For type-safe events, use EventEmitter from Codegen specs instead of NativeEventEmitter.
Error Handling
Promise Rejection
// Spec
fetchData ( url : string ): Promise < Data > ;
// iOS
- ( void )fetchData:( NSString * )url
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
if ( ! [ self isValidUrl: url]) {
reject ( @"INVALID_URL" , @"The provided URL is invalid" , nil );
return ;
}
// ... fetch data
}
// JavaScript
try {
const data = await NativeModule . fetchData ( url );
} catch ( error ) {
console . log ( error . code ); // "INVALID_URL"
console . log ( error . message ); // "The provided URL is invalid"
}
Throwing Synchronous Errors
// iOS
- ( NSString * )getValue {
if ( ! [ self isInitialized ]) {
@throw [ NSException exceptionWithName: @"NotInitialized"
reason: @"Module not initialized"
userInfo: nil ];
}
return self . value ;
}
// Android
@ReactMethod (isBlockingSynchronousMethod = true )
override fun getValue (): String {
if ( ! isInitialized) {
throw IllegalStateException ( "Module not initialized" )
}
return value
}
Minimize JSI Calls
Each JSI call has overhead. Batch operations:
// Bad - multiple JSI calls
for ( let i = 0 ; i < 1000 ; i ++ ) {
NativeModule . processItem ( items [ i ]);
}
// Good - single batched call
NativeModule . processItems ( items );
Avoid Large Data Transfers
Serializing large objects has cost:
// Bad - transfer large array
const allData = await NativeModule . getAllData (); // 10MB
// Good - paginate
const page1 = await NativeModule . getData ( 0 , 100 );
const page2 = await NativeModule . getData ( 100 , 100 );
Use ArrayBuffer for Binary Data
interface Spec extends TurboModule {
// Efficient binary data transfer
encodeImage ( data : ArrayBuffer ) : ArrayBuffer ;
}
Cache Results
// Cache constants
const constants = NativeModule . getConstants ();
// Reuse cached value
const apiUrl = constants . API_URL ;
Migration from NativeModules
Legacy NativeModule
// Old way
import { NativeModules } from 'react-native' ;
const { MyModule } = NativeModules ;
MyModule . doSomething ();
TurboModule
// New way
import NativeMyModule from './specs/NativeMyModule' ;
NativeMyModule . doSomething ();
Key Differences
Aspect NativeModules TurboModules Loading Eager Lazy Type Safety Runtime Compile-time Communication Bridge JSI Sync Methods Limited support Full support Codegen Optional Required Performance Slower Faster
Debugging
Enable TurboModule Logs
iOS:
// AppDelegate.mm
RCTSetLogThreshold (RCTLogLevelInfo);
Android:
// MainApplication.kt
ReactNativeFeatureFlags. useTurboModules ( true )
Verify TurboModule Loading
import NativeMyModule from './NativeMyModule' ;
console . log ( 'Module:' , NativeMyModule );
console . log ( 'Methods:' , Object . keys ( NativeMyModule ));
Further Reading
Codegen Learn how Codegen generates native interfaces
JavaScript Interface Understand JSI powering TurboModules
Architecture Overview See how TurboModules fit in the architecture
Threading Model Understand threading in new architecture