Native Modules Turbo Modules
Creating native modules and Turbo Modules to bridge platform-specific functionality into React Native
You are an expert in native modules for building cross-platform mobile apps with React Native. ## Key Points - Prefer the Expo Modules API over raw Turbo Modules — it handles both architectures, simplifies bridging, and provides a consistent API across platforms. - Keep native modules focused and small. One module per concern (e.g., separate modules for biometrics, keychain, Bluetooth). - Always provide TypeScript types for native module interfaces. Use codegen specs for Turbo Modules. - Return promises for any native operation that might take time (file I/O, network, keychain access). - Test native modules on real devices early. Simulators may not have all hardware capabilities. - Use events (EventEmitter) for native-to-JS communication that is ongoing or unpredictable (sensor data, push notifications, download progress). - **Threading issues**: Native module methods run on the native modules thread by default, not the main/UI thread. UI operations must be dispatched to the main thread explicitly. - **Memory leaks from event listeners**: Always remove event subscriptions in cleanup functions. Leaked native listeners can keep the entire JS context in memory. - **Type mismatches across the bridge**: The legacy bridge silently coerces types (numbers become strings). Turbo Modules with codegen catch these at build time — another reason to migrate. - **Not handling the New Architecture flag**: Libraries must support both architectures during the transition. Check `newArchEnabled` in Gradle and use `#ifdef RCT_NEW_ARCH_ENABLED` in Objective-C++. - **Blocking the JS thread with synchronous calls**: Even though Turbo Modules support sync calls, expensive operations should remain async to keep the UI responsive. - **Forgetting `expo prebuild --clean`**: After changing native module code in a managed Expo project, you must regenerate native projects or changes will not appear. ## Quick Example ```bash npx create-expo-module my-native-module ``` ```ruby # ios/Podfile additions (if not using Expo autolinking) pod 'SomeNativeSDK', '~> 3.0' ```
skilldb get react-native-skills/Native Modules Turbo ModulesFull skill: 305 linesNative Modules and Turbo Modules — React Native
You are an expert in native modules for building cross-platform mobile apps with React Native.
Core Philosophy
Native modules are the escape hatch when JavaScript cannot access a platform API directly. They exist to bridge the gap between React Native's JavaScript runtime and the native capabilities of iOS and Android. The key design principle is that the bridge should be as thin as possible: do the minimum work in native code, expose a clean asynchronous API to JavaScript, and keep business logic in the shared JavaScript layer. The less native code you write, the less platform-specific maintenance you carry.
The New Architecture (Turbo Modules and Fabric) represents a fundamental improvement over the legacy bridge. The old bridge serialized every call as JSON and sent it asynchronously across a message queue, adding latency and serialization overhead. Turbo Modules use JSI (JavaScript Interface) for direct, synchronous access to native objects in shared memory. This eliminates serialization, enables lazy module loading (modules are only initialized when first accessed, not at app startup), and provides type safety through codegen from TypeScript specs. Migrating to the New Architecture is not just a performance optimization -- it is the future of React Native.
The Expo Modules API is the practical recommendation for most native module development. It provides a unified API that works across both the legacy bridge and the New Architecture, generates boilerplate for both platforms, handles Swift/Kotlin interop cleanly, and integrates with Expo's autolinking system. Writing a native module with Expo Modules means you write approximately half the code compared to raw Turbo Modules, with the same runtime performance.
Anti-Patterns
-
Writing native modules for functionality that exists in Expo SDK: Before writing a custom native module for camera access, file system operations, secure storage, or push notifications, check if an Expo SDK module already provides it. Expo modules are maintained, tested against the current SDK version, and handle platform differences that you would otherwise need to discover and fix yourself.
-
Blocking the JavaScript thread with synchronous native calls: Even though Turbo Modules support synchronous function calls, using them for expensive operations (file I/O, crypto, image processing) blocks the JS thread and freezes the UI. Keep synchronous calls for fast, trivial operations (reading a cached config value) and use async for anything that takes measurable time.
-
Forgetting to clean up native event listeners: Native event subscriptions that are not removed when the JavaScript component unmounts keep the native listener active, holding references to the JS context and preventing garbage collection. Always return a cleanup function from
useEffectthat callssubscription.remove(). -
Not running expo prebuild --clean after changing native module code: In a managed Expo project, native projects are generated from configuration. Changes to native module source code, config plugins, or native dependencies require regeneration. Without
--clean, the stale cached project masks your changes. -
Passing unsupported types across the bridge without serialization: Custom classes, enums,
Dateobjects, and complex nested types are not supported by the standard message codec. They must be serialized to supported primitives (strings, numbers, maps, lists) or, better, use Pigeon (Flutter) or codegen (Turbo Modules) for type-safe automatic serialization.
Overview
Native modules allow React Native apps to call platform-specific code (Swift/Kotlin/Java/Objective-C) from JavaScript. The legacy Bridge architecture serializes all calls as JSON over an asynchronous bridge. The New Architecture (Turbo Modules) uses JSI (JavaScript Interface) for synchronous, direct communication between JS and native code, eliminating serialization overhead. Expo Modules API provides a unified, simpler way to write native modules that work in both architectures.
Core Concepts
Architecture Comparison
| Feature | Legacy Bridge | Turbo Modules (New Architecture) |
|---|---|---|
| Communication | Async JSON bridge | Synchronous JSI |
| Type Safety | Runtime validation | Codegen from spec |
| Lazy Loading | All modules loaded at startup | Loaded on first access |
| Performance | Serialization overhead | Direct memory access |
Expo Modules API (Recommended)
The simplest way to write a native module for Expo and bare React Native projects:
npx create-expo-module my-native-module
modules/my-native-module/
src/
MyNativeModule.ts # TypeScript wrapper
MyNativeModule.web.ts # Web implementation (optional)
ios/
MyNativeModule.swift # iOS implementation
android/
src/main/java/.../
MyNativeModule.kt # Android implementation
expo-module.config.json
iOS Implementation (Swift via Expo Modules)
// ios/MyNativeModule.swift
import ExpoModulesCore
public class MyNativeModule: Module {
public func definition() -> ModuleDefinition {
Name("MyNativeModule")
// Synchronous function
Function("getDeviceModel") { () -> String in
return UIDevice.current.model
}
// Async function
AsyncFunction("fetchSecureToken") { (key: String, promise: Promise) in
guard let token = KeychainHelper.get(key: key) else {
promise.reject("NOT_FOUND", "Token not found for key: \(key)")
return
}
promise.resolve(token)
}
// Events
Events("onBatteryChange")
// View (native UI component)
View(MyNativeView.self) {
Prop("cornerRadius") { (view: MyNativeView, radius: Double) in
view.layer.cornerRadius = radius
}
Events("onTap")
}
}
}
Android Implementation (Kotlin via Expo Modules)
// android/src/main/java/com/example/MyNativeModule.kt
package com.example.mynativemodule
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import android.os.Build
class MyNativeModule : Module() {
override fun definition() = ModuleDefinition {
Name("MyNativeModule")
Function("getDeviceModel") {
Build.MODEL
}
AsyncFunction("fetchSecureToken") { key: String ->
val token = KeystoreHelper.get(key)
?: throw Exception("Token not found for key: $key")
token
}
Events("onBatteryChange")
}
}
TypeScript Wrapper
// src/MyNativeModule.ts
import { requireNativeModule } from "expo-modules-core";
interface MyNativeModuleInterface {
getDeviceModel(): string;
fetchSecureToken(key: string): Promise<string>;
addListener(eventName: string): void;
removeListeners(count: number): void;
}
const NativeModule =
requireNativeModule<MyNativeModuleInterface>("MyNativeModule");
export function getDeviceModel(): string {
return NativeModule.getDeviceModel();
}
export async function fetchSecureToken(key: string): Promise<string> {
return NativeModule.fetchSecureToken(key);
}
export { NativeModule };
Implementation Patterns
Turbo Module with Codegen (New Architecture)
For projects using the New Architecture directly (without Expo Modules):
// specs/NativeCalculator.ts
import type { TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";
export interface Spec extends TurboModule {
add(a: number, b: number): number; // Synchronous
multiply(a: number, b: number): Promise<number>; // Async
getConstants(): {
PI: number;
E: number;
};
}
export default TurboModuleRegistry.getEnforcing<Spec>("NativeCalculator");
// Android implementation
package com.example
import com.facebook.react.bridge.ReactApplicationContext
import com.example.NativeCalculatorSpec
class NativeCalculatorModule(context: ReactApplicationContext) :
NativeCalculatorSpec(context) {
override fun getName() = "NativeCalculator"
override fun add(a: Double, b: Double): Double = a + b
override fun multiply(a: Double, b: Double, promise: Promise) {
promise.resolve(a * b)
}
override fun getTypedExportedConstants(): Map<String, Any> = mapOf(
"PI" to Math.PI,
"E" to Math.E,
)
}
Fabric Native Component (New Architecture UI)
// specs/NativeMapView.ts
import type { ViewProps } from "react-native";
import type {
Float,
DirectEventHandler,
} from "react-native/Libraries/Types/CodegenTypes";
import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNativeComponent";
interface NativeMapViewProps extends ViewProps {
latitude: Float;
longitude: Float;
zoomLevel: Float;
onRegionChange?: DirectEventHandler<{
latitude: Float;
longitude: Float;
}>;
}
export default codegenNativeComponent<NativeMapViewProps>("NativeMapView");
Native Event Emitters
// Using Expo Modules events
import { EventEmitter, Subscription } from "expo-modules-core";
import { NativeModule } from "./MyNativeModule";
const emitter = new EventEmitter(NativeModule);
export function onBatteryChange(
callback: (event: { level: number; isCharging: boolean }) => void
): Subscription {
return emitter.addListener("onBatteryChange", callback);
}
// Usage in component
function BatteryIndicator() {
const [battery, setBattery] = useState({ level: 1, isCharging: false });
useEffect(() => {
const subscription = onBatteryChange(setBattery);
return () => subscription.remove();
}, []);
return <Text>Battery: {Math.round(battery.level * 100)}%</Text>;
}
Linking Existing Native Libraries
// expo-module.config.json
{
"platforms": ["ios", "android"],
"ios": {
"modules": ["MyNativeModule"]
},
"android": {
"modules": ["com.example.mynativemodule.MyNativeModule"]
}
}
# ios/Podfile additions (if not using Expo autolinking)
pod 'SomeNativeSDK', '~> 3.0'
// android/build.gradle
dependencies {
implementation 'com.some.native:sdk:3.0.0'
}
Best Practices
- Prefer the Expo Modules API over raw Turbo Modules — it handles both architectures, simplifies bridging, and provides a consistent API across platforms.
- Keep native modules focused and small. One module per concern (e.g., separate modules for biometrics, keychain, Bluetooth).
- Always provide TypeScript types for native module interfaces. Use codegen specs for Turbo Modules.
- Return promises for any native operation that might take time (file I/O, network, keychain access).
- Test native modules on real devices early. Simulators may not have all hardware capabilities.
- Use events (EventEmitter) for native-to-JS communication that is ongoing or unpredictable (sensor data, push notifications, download progress).
Common Pitfalls
- Threading issues: Native module methods run on the native modules thread by default, not the main/UI thread. UI operations must be dispatched to the main thread explicitly.
- Memory leaks from event listeners: Always remove event subscriptions in cleanup functions. Leaked native listeners can keep the entire JS context in memory.
- Type mismatches across the bridge: The legacy bridge silently coerces types (numbers become strings). Turbo Modules with codegen catch these at build time — another reason to migrate.
- Not handling the New Architecture flag: Libraries must support both architectures during the transition. Check
newArchEnabledin Gradle and use#ifdef RCT_NEW_ARCH_ENABLEDin Objective-C++. - Blocking the JS thread with synchronous calls: Even though Turbo Modules support sync calls, expensive operations should remain async to keep the UI responsive.
- Forgetting
expo prebuild --clean: After changing native module code in a managed Expo project, you must regenerate native projects or changes will not appear.
Install this skill directly: skilldb add react-native-skills
Related Skills
Reanimated Animations
High-performance animations in React Native using Reanimated and Gesture Handler
Eas Build Ota Updates
Deploying React Native apps with EAS Build, app store submission, and OTA updates via EAS Update
Expo Managed Workflow
Expo managed workflow for rapid React Native development with minimal native configuration
React Navigation Patterns
React Navigation patterns for stack, tab, drawer, and nested navigators in React Native
Offline Storage
Offline storage strategies in React Native using AsyncStorage, MMKV, and WatermelonDB
State Management Zustand Jotai
State management in React Native using Zustand and Jotai for scalable, performant app state