Building TurboModules in React Native: Writing Native Code That Actually Works
Step-by-step guide to building React Native TurboModules with JSI — TypeScript spec, Swift and Kotlin implementation, and CodeGen setup.
You need to access a native SDK, a hardware peripheral, or a platform API that no existing React Native package covers. Writing your own native module is the right answer — but the correct way to write one has changed significantly with the New Architecture.
The old RCT_EXPORT_MODULE and @ReactMethod pattern is deprecated. TurboModules are the replacement. This guide walks through writing a TurboModule from the TypeScript spec through to working Swift and Kotlin implementations.
What TurboModules Are and Why They Replace the Old Bridge
The legacy native module system serialized every JavaScript call to JSON, passed it across an asynchronous bridge to the native side, and returned the result the same way. Synchronous calls were not possible, and the serialization overhead was measurable.
TurboModules use JSI (JavaScript Interface) — a C++ layer that lets JavaScript hold direct references to native objects. This means:
- Synchronous calls to native code are possible where appropriate
- No JSON serialization overhead for type-safe values
- Modules are lazy-loaded and only initialized when JavaScript first uses them
- Types flow from a single TypeScript spec through generated C++ bindings to native implementations
Step 1: Write the TypeScript Spec
The TypeScript spec is the source of truth for your module's interface. CodeGen reads it and generates the C++ glue code that binds your JavaScript API to the native implementations.
Create a file named NativeMyModule.ts (the Native prefix is required):
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getDeviceInfo(): Promise<{ model: string; osVersion: string }>;
computeHash(input: string): string;
}
export default TurboModuleRegistry.strictGet<Spec>('MyModule');Rules for the spec file:
- Filename must match
Native[ModuleName].ts - The interface must extend
TurboModule - All types must be expressible in the CodeGen type system — no union types with complex shapes, no recursive types
- Synchronous methods return values directly; async methods return
Promise
Step 2: Configure CodeGen
In package.json, add the CodeGen configuration:
{
"codegenConfig": {
"name": "MyModuleSpecs",
"type": "modules",
"jsSrcsDir": "./src"
}
}Point jsSrcsDir to the directory containing your NativeMyModule.ts file.
Run cd android && ./gradlew generateCodegenArtifactsFromSchema to generate the Android bindings, and cd ios && RCT_NEW_ARCH_ENABLED=1 pod install to generate the iOS bindings.
Step 3: iOS Implementation in Swift
Create MyModule.swift and MyModuleImpl.m:
The Swift implementation:
import Foundation
@objc(MyModule)
class MyModule: NSObject {
@objc func getDeviceInfo(
_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {
let info: [String: String] = [
"model": UIDevice.current.model,
"osVersion": UIDevice.current.systemVersion
]
resolve(info)
}
@objc func computeHash(_ input: String) -> String {
// synchronous — returns directly
return input.data(using: .utf8).map {
$0.map { String(format: "%02x", $0) }.joined()
} ?? ""
}
}For the New Architecture, you also implement the generated protocol from CodeGen. The generated protocol name follows the pattern NativeMyModuleSpec. Your Swift class must conform to it.
Register the module in a separate Objective-C file to satisfy React Native's module registration system:
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(MyModule, NSObject)
RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(computeHash:(NSString *)input)
@endStep 4: Android Implementation in Kotlin
On Android, implement the generated abstract class from CodeGen. The generated class name follows the pattern NativeMyModuleSpec:
package com.myapp
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.Promise
class MyModule(reactContext: ReactApplicationContext) :
NativeMyModuleSpec(reactContext) {
override fun getName() = NAME
override fun getDeviceInfo(promise: Promise) {
val info = mapOf(
"model" to android.os.Build.MODEL,
"osVersion" to android.os.Build.VERSION.RELEASE
)
promise.resolve(
com.facebook.react.bridge.Arguments.makeNativeMap(info)
)
}
override fun computeHash(input: String): String {
return input.toByteArray()
.joinToString("") { "%02x".format(it) }
}
companion object {
const val NAME = "MyModule"
}
}Register the package in your MainApplication:
packages.add(MyModulePackage())Step 5: Using the Module from JavaScript
Back in JavaScript, import from your spec file:
import NativeMyModule from './NativeMyModule';
const info = await NativeMyModule.getDeviceInfo();
console.log(info.model, info.osVersion);
const hash = NativeMyModule.computeHash('hello');TypeScript will enforce the correct argument types and return types based on the spec you wrote.
Using the Expo Modules API Instead
If the CodeGen and protocol conformance boilerplate above feels like a lot, the Expo Modules API provides a higher-level abstraction over the same JSI foundation. It lets you define your module in a declarative Kotlin/Swift DSL without writing the spec file separately or managing CodeGen configuration manually.
For modules you are shipping as open-source packages, the Expo Modules API is the better default in 2026 — it handles the New Architecture compatibility surface for you. For internal modules in a bare React Native project where you control the full build, either approach works.
Testing Native Modules
Write your native implementation tests in the native testing framework (XCTest for iOS, JUnit for Android) to catch errors before they surface in JavaScript. On the JavaScript side, mock the native module in Jest:
jest.mock('./NativeMyModule', () => ({
getDeviceInfo: jest.fn().mockResolvedValue({ model: 'iPhone', osVersion: '17' }),
computeHash: jest.fn().mockReturnValue('aabbcc'),
}));This keeps your JavaScript logic tests fast and independent of native builds.
TurboModules are straightforward once you have the pattern established. The TypeScript spec as the source of truth is a genuine improvement over the old approach — types flow from a single definition to both native implementations rather than being duplicated and able to drift.
If you need a team to build or integrate a native module — Bluetooth, hardware SDKs, payment processors, biometrics — Clixo handles this end-to-end.