Skip to main content
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
}

Performance Considerations

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

AspectNativeModulesTurboModules
LoadingEagerLazy
Type SafetyRuntimeCompile-time
CommunicationBridgeJSI
Sync MethodsLimited supportFull support
CodegenOptionalRequired
PerformanceSlowerFaster

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

Build docs developers (and LLMs) love