Introduction
Turbo Modules represent a significant advancement in React Native's architecture, enabling type-safe, high-performance communication between JavaScript and native code. As part of React Native's New Architecture, Turbo Modules replace the legacy bridge with a modern, C++-based communication layer that offers improved performance, better type safety, and enhanced developer experience.
This comprehensive guide walks you through building custom Turbo Modules for Android, covering everything from TypeScript specifications to native implementation, equipping you with the knowledge to leverage platform-specific capabilities in your cross-platform mobile applications. By the end of this guide, you'll understand how to create type-safe native modules that integrate seamlessly with your React Native application while maintaining optimal performance.
Turbo Modules are the modern native module system introduced with React Native's New Architecture. Unlike their legacy counterparts, Turbo Modules use code generation to create type-safe interfaces between JavaScript and native code, eliminating the need for manual bridging and reducing runtime overhead. The system relies on a codegen tool that reads TypeScript or Flow specifications and generates the necessary C++, Java, and Objective-C boilerplate code to facilitate communication between layers. This approach brings several advantages over traditional native modules, including compile-time type checking, better error messages, and improved performance through lazy loading of native modules.
The architecture of Turbo Modules centers around the concept of a specification file that declares the interface between JavaScript and native code. This specification serves as a single source of truth that both sides agree upon, ensuring that any mismatches between expected and actual types are caught early in the development process. When the application builds, React Native's codegen reads these specifications and generates the appropriate native interfaces, callback wrappers, and promise handlers. The generated code handles the complexities of JSI (JavaScript Interface) communication, allowing developers to focus on implementing their native functionality rather than managing cross-language interactions.
Key advantages of using Turbo Modules in React Native projects
Type Safety
Codegen creates type-safe interfaces between JavaScript and native code, catching errors at compile time rather than runtime. This eliminates entire categories of bugs related to type mismatches and missing parameters.
Lazy Loading
Turbo Modules load on-demand, reducing application startup time by only initializing modules when they're first accessed. This lazy initialization is particularly beneficial for applications with many native modules.
Performance
JSI-based communication eliminates bridge serialization overhead, enabling direct, efficient cross-language calls. Values pass directly between JavaScript and native code without JSON conversion.
Developer Experience
Full TypeScript support with IDE autocompletion and compile-time checking for all native module methods. Get immediate feedback on incorrect usage before runtime.
Creating TypeScript Specifications
The specification file is the foundation of any Turbo Module and defines the contract between JavaScript and native code. Written in TypeScript, this file declares the methods, properties, and callbacks that your module will expose to JavaScript. The specification uses the TurboModule base type from React Native and extends it with your custom interface, creating a type-safe foundation for your native module. The TurboModuleRegistry then uses this specification to generate the appropriate type definitions that JavaScript code can use with full type safety. The TypeScript integration provides superior developer experience with full IDE support.
Specification Structure
A typical specification file begins by importing the necessary types from React Native and defining an interface that extends TurboModule. Each method is declared with proper TypeScript types, including parameters and return types. For asynchronous operations, you can use either callbacks or promises, with promises being the modern and recommended approach. The specification also defines constants and any read-only properties that the module exposes. Each method in the interface corresponds to a native function that will be implemented on the Android side.
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
// Synchronous methods
getName(): string;
// Asynchronous methods with callbacks
getDeviceInfo(callback: (error: ?string, result: ?string) => void): void;
// Asynchronous methods with promises
getBatteryLevel(): Promise<number>;
// Methods with multiple parameters
saveData(key: string, value: string): void;
retrieveData(key: string): string | null;
// Event listeners
addListener(eventName: string): void;
removeListeners(count: number): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeDeviceInfo');
Export Patterns
The export pattern is crucial for Turbo Modules, as the TurboModuleRegistry uses these exports to discover and load your module. The getEnforcing method ensures that your module is available, throwing an error if it cannot be found, while the get method returns null if the module is unavailable, allowing for graceful fallbacks. Your module name in the registry should match the name used in the native implementation, typically following the "Native" prefix convention for clarity. This registration-based approach means React Native automatically discovers your module without manual linking.
The TurboModuleRegistry provides automatic module discovery and lifecycle management. When your module is first imported from JavaScript, the registry initializes it and caches the instance for subsequent accesses. This lazy initialization reduces application startup time, as modules are only created when they're actually needed. The module loading is handled automatically by the TurboModuleRegistry, which manages the lifecycle of each module and ensures that native code is only executed when necessary.
Before creating a Turbo Module, you need to ensure your React Native project is configured to use the New Architecture. This requires React Native version 0.71 or later, as these versions have stable support for Turbo Modules. The New Architecture must be explicitly enabled in your project settings, which can typically be done through the project configuration or by setting the appropriate flags in your build configuration. For Android projects, this involves modifying the build.gradle files to enable the new architecture and configure codegen properly.
Configuring Codegen
React Native's codegen is the bridge that transforms your TypeScript specifications into native code interfaces. This automatic code generation is what makes Turbo Modules type-safe and reduces the boilerplate code developers need to write. The codegen configuration is specified in your package.json file, where you define the name of your spec, the type of artifact to generate, the location of your specs, and platform-specific configuration like the Java package name for Android.
Package Configuration
The codegenConfig section in package.json tells React Native's build tools what to generate and where to find the source specifications. The name field identifies your module in the generated code, the type field specifies that you're creating modules rather than components, and the jsSrcsDir points to the directory containing your TypeScript specifications. For Android, the javaPackageName must match the Java package structure you use in your native implementation, as this determines where the generated Java interfaces will be placed.
{
"name": "react-native-device-info",
"codegenConfig": {
"name": "DeviceInfoSpec",
"type": "modules",
"jsSrcsDir": "src",
"android": {
"javaPackageName": "com.reactnativedeviceinfo"
}
}
}
How Codegen Works
When you build your Android project, the codegen automatically runs as part of the Gradle build process through the generateCodegenArtifactsFromSchema task. This task reads your TypeScript specifications and generates Java interfaces that your native implementation must extend. The generated code includes base classes with stub implementations for each method declared in your specification, type converters for JavaScript values, and the infrastructure for communicating with the JavaScript runtime through JSI.
The generated code handles the complexities of JSI (JavaScript Interface) communication, allowing developers to focus on implementing their native functionality rather than managing cross-language interactions. You can verify that codegen is working correctly by checking the build output for messages about generating artifacts from your specifications. The codegen tool creates the necessary C++, Java, and Kotlin boilerplate code to facilitate communication between layers, bringing several advantages including compile-time type checking, better error messages, and improved performance through lazy loading of native modules.
Implementing Native Android Module in Kotlin
Kotlin is the recommended language for implementing Turbo Modules on Android, offering better integration with modern Android development practices and more expressive syntax than Java. Your Kotlin implementation extends the generated spec class and provides concrete implementations for each method declared in the TypeScript specification. The implementation class must be annotated with the ReactModule annotation to register it with the React Native runtime, and it must be discoverable through React Package registration.
Module Class Structure
The implementation class receives a ReactApplicationContext in its constructor, which provides access to the Android application context and React Native internals. This context is necessary for accessing system services, resources, and other Android APIs that your Turbo Module needs to interact with. Each method from the specification is overridden with a concrete implementation that performs the actual native functionality. Synchronous methods return values directly, while asynchronous methods use callbacks or return promises that resolve with the result.
package com.reactnativedeviceinfo
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.reactnativedeviceinfo.generated.DeviceInfoSpec
@ReactModule(name = NativeDeviceInfo.NAME)
class NativeDeviceInfoModule(
private val reactContext: ReactApplicationContext
) : DeviceInfoSpec(reactContext) {
companion object {
const val NAME = "NativeDeviceInfo"
}
override fun getName(): String {
return NAME
}
override fun getDeviceInfo(callback: (String?, String?) -> Unit) {
try {
val deviceInfo = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}"
callback(null, deviceInfo)
} catch (e: Exception) {
callback(e.message, null)
}
}
override fun getBatteryLevel(): Promise {
val promise = Promise()
try {
val batteryIntent = reactContext.registerReceiver(
null,
android.content.IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
val level = batteryIntent?.getIntExtra(
android.content.Intent.EXTRA_LEVEL, -1
) ?: -1
val scale = batteryIntent?.getIntExtra(
android.content.Intent.EXTRA_SCALE, -1
) ?: -1
val batteryLevel = if (level >= 0 && scale > 0) {
level.toDouble() / scale.toDouble()
} else {
0.0
}
promise.resolve(batteryLevel)
} catch (e: Exception) {
promise.reject("BATTERY_ERROR", "Failed to get battery level", e)
}
return promise
}
override fun saveData(key: String, value: String) {
val sharedPrefs = reactContext.getSharedPreferences(
"TurboModuleData",
android.content.Context.MODE_PRIVATE
)
sharedPrefs.edit().putString(key, value).apply()
}
override fun retrieveData(key: String): String? {
val sharedPrefs = reactContext.getSharedPreferences(
"TurboModuleData",
android.content.Context.MODE_PRIVATE
)
return sharedPrefs.getString(key, null)
}
}
Key Implementation Details
The @ReactModule annotation registers your module with the React Native runtime. The companion object defines the NAME constant used for module identification throughout the system. Promise-based methods provide clean asynchronous handling with proper error rejection codes and messages. The ReactApplicationContext passed to your constructor enables access to Android system services and APIs.
The Promise API in Turbo Modules provides a cleaner alternative to callbacks for asynchronous operations. Your implementation receives a Promise object that you can resolve with the successful result or reject with an error code, message, and optional exception. This approach integrates naturally with Kotlin's coroutines and provides better stack traces for debugging. The promise-based approach is particularly useful for operations that involve network requests, file I/O, or other potentially slow operations that would benefit from non-blocking execution.
Java Implementation Alternative
While Kotlin is recommended, Java implementations remain fully supported for Turbo Modules. The Java implementation follows a similar pattern to Kotlin but with more verbose syntax due to Java's language constraints. The implementation class extends the generated spec class and provides method overrides for each declared function. Java implementations must carefully handle the conversion between JavaScript values and Java types, using the ReactApplicationContext for accessing Android APIs.
package com.reactnativedeviceinfo;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.reactnativedeviceinfo.generated.DeviceInfoSpec;
@ReactModule(name = NativeDeviceInfoModule.NAME)
public class NativeDeviceInfoModule extends DeviceInfoSpec {
public static final String NAME = "NativeDeviceInfo";
private final ReactApplicationContext reactContext;
public NativeDeviceInfoModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@NonNull
@Override
public String getName() {
return NAME;
}
@Override
public void getDeviceInfo(Callback callback) {
try {
String deviceInfo = android.os.Build.MANUFACTURER + " " +
android.os.Build.MODEL;
callback.invoke(null, deviceInfo);
} catch (Exception e) {
callback.invoke(e.getMessage(), null);
}
}
@Override
public void getBatteryLevel(Promise promise) {
try {
Intent batteryIntent = reactContext.registerReceiver(
null,
new IntentFilter(Intent.ACTION_BATTERY_CHANGED)
);
int level = batteryIntent.getIntExtra(Intent.EXTRA_LEVEL, -1);
int scale = batteryIntent.getIntExtra(Intent.EXTRA_SCALE, -1);
double batteryLevel = (level >= 0 && scale > 0) ?
(double) level / scale : 0.0;
promise.resolve(batteryLevel);
} catch (Exception e) {
promise.reject("BATTERY_ERROR", "Failed to get battery level", e);
}
}
}
Java Considerations
Java implementations require explicit null handling and type checking, as Java lacks Kotlin's null safety features. The Callback interface is used for traditional asynchronous operations with error-first callbacks, where the first parameter is an error (or null if successful) and subsequent parameters contain the result data. This pattern is familiar to developers coming from Node.js or older JavaScript codebases.
Native implementations can often be adapted with minimal changes, as the core logic of accessing Android APIs remains the same. The main differences are in how the module is structured (extending the generated spec instead of ReactContextBaseJavaModule) and how asynchronous results are communicated (using promises or callbacks with the generated infrastructure). The ReactModule annotation replaces the @ReactMethod annotations used in legacy modules, providing similar functionality with a different syntax.
Registering Your Module
For your Turbo Module to be discovered by React Native, you need to register it through a React Package. This registration process creates the connection between the React Native runtime and your native module, allowing JavaScript code to successfully instantiate and call your module. The package implementation creates instances of your module when requested and manages the lifecycle of these instances throughout the application lifecycle.
Creating the Package
The ReactPackage interface requires implementing the createNativeModules method, which returns a list of native modules to be registered. Your Turbo Module instance is created here and added to the module list. The package also handles views and modules that need to be exposed to JavaScript, though Turbo Modules only require the native module registration. This package must be added to the list of packages in your MainApplication class for it to be properly initialized when the application starts.
package com.reactnativedeviceinfo
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class DeviceInfoPackage : ReactPackage {
override fun createNativeModules(
reactContext: ReactApplicationContext
): List<NativeModule> {
return listOf(NativeDeviceInfoModule(reactContext))
}
override fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<*, *>> {
return emptyList()
}
}
MainApplication Integration
In your MainApplication class, you need to add your package to the list of packages that React Native initializes during startup. This is typically done in the getPackages method of MainApplication, where packages are instantiated and added to a list. The order of packages in this list generally does not matter, but it's good practice to keep related packages together and document any dependencies between packages.
override fun getPackages(): List<ReactPackage> {
return PackageList(this).packages.apply {
add(DeviceInfoPackage())
}
}
The package registration ensures your module is instantiated when React Native initializes, making it available for JavaScript code to import and use. This registration-based approach means React Native automatically discovers your module without manual linking, simplifying the integration process for both library authors and application developers.
Using Turbo Modules from JavaScript
Once your Turbo Module is implemented and registered, you can use it from JavaScript code just like any other module imported from React Native. The TurboModuleRegistry handles loading the native module and providing you with a typed interface for calling native methods. Using the getEnforcing method ensures that your application fails fast if the native module is not available, which is useful during development and helps identify configuration issues early.
Type-Safe Usage
The type safety provided by Turbo Modules means that you get full TypeScript checking when calling native methods. Parameter types are validated at compile time, return types are correctly inferred, and you get IDE autocompletion for all available methods and their signatures. This is a significant improvement over legacy native modules, where type information was often missing or incomplete, leading to runtime errors that were difficult to diagnose. By leveraging TypeScript-based development, you ensure type-safe interactions across your entire application.
import NativeDeviceInfo from './src/NativeDeviceInfo';
async function initializeApp() {
// Get synchronous module instance
const moduleName = NativeDeviceInfo.getName();
console.log(`Module name: ${moduleName}`);
// Use callback-based asynchronous method
NativeDeviceInfo.getDeviceInfo((error, result) => {
if (error) {
console.error('Failed to get device info:', error);
} else {
console.log(`Device: ${result}`);
}
});
// Use promise-based asynchronous method
try {
const batteryLevel = await NativeDeviceInfo.getBatteryLevel();
console.log(`Battery: ${(batteryLevel * 100).toFixed(0)}%`);
} catch (error) {
console.error('Battery check failed:', error);
}
// Store and retrieve data
NativeDeviceInfo.saveData('userToken', 'abc123');
const token = NativeDeviceInfo.retrieveData('userToken');
console.log(`Retrieved token: ${token}`);
}
Turbo Modules provide complete TypeScript type information for all methods, enabling IDE autocompletion and compile-time type checking. Parameter types are validated, return types are inferred, and you receive immediate feedback on incorrect usage. The Promise-based async operations integrate naturally with async/await syntax, making your code more readable and maintainable. This type-safe approach eliminates entire categories of bugs related to type mismatches and missing parameters.
Performance Considerations
Lazy Loading Benefits
Turbo Modules offer significant performance improvements over legacy native modules, but realizing these benefits requires understanding how the system works under the hood. The lazy loading mechanism means that native modules are only initialized when first accessed, reducing application startup time. However, this also means that the first call to any method in a module will incur the initialization cost, which could cause noticeable delays if not anticipated. Plan your usage patterns accordingly, and consider eagerly initializing critical modules during app startup if startup timing is important.
JSI Communication Efficiency
The JSI-based communication layer eliminates the serialization overhead of the legacy bridge, where JSON serialization and deserialization were required for every cross-language call. With Turbo Modules, values can be passed directly between JavaScript and native code without conversion in many cases, particularly for primitive types and typed arrays. This direct communication is particularly beneficial for modules that transfer large amounts of data or are called frequently. The JSI-based layer eliminates bridge serialization overhead, enabling direct, efficient cross-language calls.
Optimization Strategies
When designing your Turbo Module, consider the frequency and pattern of method calls from JavaScript. Methods that are called very frequently should be optimized to minimize overhead, potentially by batching operations or by providing synchronous access patterns where appropriate. For methods that involve expensive operations, offloading to background threads and returning results asynchronously prevents blocking the JavaScript thread and keeps the UI responsive. Following these mobile development best practices ensures optimal app performance.
- Use promises for async operations - Prevent blocking the JavaScript thread during native operations
- Offload expensive work - Use background threads for file I/O, network requests, and computations
- Batch operations - Combine multiple related operations into single native calls
- Cache frequently accessed data - Avoid repeatedly querying native APIs for the same information
- Consider native-to-JavaScript callbacks - Use event-driven patterns for streaming data updates
Migration from Legacy Native Modules
Migrating existing legacy native modules to Turbo Modules is a common task as more projects adopt the New Architecture. The migration process involves several steps: creating a TypeScript specification for your module's interface, implementing the generated interface in your native code, updating the package registration to use the new patterns, and testing that all functionality works correctly with the Turbo Module system.
Migration Steps
-
Create a TypeScript specification mirroring your legacy module's interface, reviewing the JavaScript code that uses your module and ensuring all methods, properties, and event listeners are properly declared
-
Implement the generated interface in your native code, adapting the core logic while leveraging the better type safety throughout the call chain
-
Update package registration to use the new ReactPackage patterns with the @ReactModule annotation replacing @ReactMethod annotations
-
Test thoroughly to verify JavaScript-to-native communication works correctly with the Turbo Module system
Key Differences from Legacy Modules
The specification for a migrated module should mirror the interface that was previously available through the legacy module. Specifications replace manual bridging code entirely, with Codegen creating the necessary infrastructure. The @ReactModule annotation replaces @ReactMethod annotations, providing similar functionality with a different syntax. Generated base classes provide JSI infrastructure that handles cross-language communication. Promise-based APIs are preferred over callbacks in modern code, though both patterns remain supported.
Compatibility Considerations
Legacy native modules and Turbo Modules can coexist during migration, allowing gradual migration of your application's modules. This means you don't need to migrate everything at once - you can convert modules incrementally while maintaining application functionality. However, new modules should use Turbo Module patterns for consistency and performance benefits. The generated code provides better type safety than manual bridging, so take advantage of this by being precise about types and including documentation comments in your specification.
Frequently Asked Questions
Sources
-
React Native: Turbo Native Modules Introduction - Official documentation for creating Turbo Native Modules that interact with native platform APIs not provided by React Native core.
-
React Native: Android Native Modules (Legacy) - Reference for understanding legacy approaches before the New Architecture and migration patterns.
-
React Native New Architecture Discussions - Community discussions on Turbo Module implementation, common patterns, and migration strategies.