React Native is getting faster and cleaner. With the New Architecture, you don’t need to write tons of bridging code anymore. Thanks to Codegen and TurboModules, we now have:
✅ Automatic native binding
✅ Type-safe communication
✅ Better performance
In this guide, we’ll break down the concepts and build a real-world example module—step-by-step for both Android and iOS—that demonstrates:
✅ Sending a message from JS → Native
✅ Getting a message from Native → JS (via Promise)
✅ Using callbacks to return values
✅ Emitting real-time events from Native → JS
Let’s dive in. 👇
📘 What’s Codegen?
Codegen is the code generation system that automatically creates native interface files from TypeScript specifications.
✅ What Codegen Does:
- Reads TypeScript spec files (like
specs/NativeMessageModule.ts
) - Generates native interface files for both Android and iOS
- Creates type-safe bindings between JavaScript and native code
- Handles the boilerplate code automatically
⚙️ What’s TurboModules?
Turbo Modules is the runtime architecture that provides fast, type-safe communication between JavaScript and native code.
✅ What Turbo Modules Does:
- Provides the runtime infrastructure for native-JS communication
- Implements the TurboModule interface
- Offers better performance than the legacy bridge
- Enables synchronous calls and better memory management
Aspect | Old Bridge | New (TurboModules + Codegen) |
---|---|---|
Manual bridging | ✅ Required | ❌ Auto-generated via Codegen |
Performance | 🐢 Slower | 🚀 Much faster |
Type safety | ❌ Risk of mismatch | ✅ Enforced via TypeScript |
Native ↔ JS | Async only | Async + Sync support |
Why Both Are Needed
- Without Codegen: You'd have to manually write all the interface files
- Without Turbo Modules: You'd still use the slow legacy bridge system
- With Both: You get automatic code generation + fast runtime performance
Think of it this way:
- Codegen = The factory that builds the blueprint
- Turbo Modules = The engine that makes everything run fast
⚙️ My Current Configuration
Here's the exact setup I used for testing these solutions:
{
"dependencies": {
"react": "19.0.0",
"react-native": "0.79.3"
}
}
📁 Project Structure Overview
Here's how to structure your project to support TurboModules:
yourapp/
├── android/
│ └── app/
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── sampleapp/
│ ├── NativeMessageModule.java
│ ├── NativeMessagePackage.java
│ └── MainApplication.java
├── ios/
│ ├── NativeMessageModule.h
│ └── NativeMessageModule.mm
├── specs/
│ └── NativeMessageModule.ts
├── App.js
├── package.json
└── ...
✅ Step-by-Step Android Example
1. 📄 specs/NativeMessageModule.ts
This is your TypeScript interface spec — the source of truth for Codegen.
import {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import {TurboModuleRegistry} from 'react-native';
import { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
export interface Spec extends TurboModule {
sendMessage(message: string): void;
getMessage(): Promise<string>;
sendWithCallback(callback: (response: string) => void): void;
addListener(eventName: string): void;
removeListeners(count: number): void;
startSendingEvents(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeMessageModule');
All Native Turbo Module spec files must have the prefix
Native
, otherwise Codegen will ignore them.The string 'NativeMessageModule' must exactly match
public static final String NAME = "NativeMessageModule";
in your Java module. This consistency is critical for Codegen and TurboModules to connect properly.
2. 🧠 codegenConfig in package.json
Tells Codegen how to generate files.
"codegenConfig": {
"name": "NativeMessageModule",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.turbomodules" #Replace this with your packagename
}
}
android
Codegen is executed through the generateCodegenArtifactsFromSchema Gradle task:
cd android
./gradlew generateCodegenArtifactsFromSchema
BUILD SUCCESSFUL in 837ms
14 actionable tasks: 3 executed, 11 up-to-date
This is automatically run when you build your Android application.
iOS
You need to install the pods to make sure that codegen runs to generate the new files:
cd ios
pod install
3. 🧱 NativeMessageModule.java
✅ File: NativeMessageModule.java
📍 Path: android/app/src/main/java/com/sampleapp/NativeMessageModule.java
This is your real native logic class.
package com.turbomodules; #Replace this with your packagename
package com.turbomodules;
import android.os.Handler;
import android.os.Looper;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;
@ReactModule(name = NativeMessageModule.NAME)
public class NativeMessageModule extends NativeMessageModuleSpec {
public static final String NAME = "NativeMessageModule";
public NativeMessageModule(ReactApplicationContext reactContext) {
super(reactContext);
}
// 1. Send message (no return)
public void sendMessage(String message) {
System.out.println("Received from JS: " + message);
}
// 2. Return message using Promise
public void getMessage(Promise promise) {
try {
String message = "Hello from Android";
promise.resolve(message);
} catch (Exception e) {
promise.reject("GET_MESSAGE_ERROR", e);
}
}
// 3. Return message using Callback
public void sendWithCallback(Callback callback) {
String response = "Callback response from Android";
callback.invoke(response);
}
// 4. Send event to JS
@Override
public void addListener(String eventName) {
// Required for Turbo Module event emitters
// This is called automatically when JS adds a listener
}
// 5. Remove event listeners
@Override
public void removeListeners(double count) {
// Required for Turbo Module event emitters
// This is called automatically when JS removes listeners
}
// 6. Emit an event to JS
@ReactMethod
public void startSendingEvents() {
Handler handler = new Handler(Looper.getMainLooper());
for (int i = 1; i <= 5; i++) {
int delay = i * 1000;
handler.postDelayed(() -> {
WritableMap map = Arguments.createMap();
map.putString("message", "Event at " + System.currentTimeMillis());
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("onTimerTick", map);
}, delay);
}
}
}
4. 📦 NativeMessagePackage.java
✅ File: NativeMessagePackage.java
📍 Path:
android/app/src/main/java/com/sampleapp/NativeMessagePackage.java
Registers your module in the app.
package com.turbomodules; #Replace this with your packagename
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.*;
import com.facebook.react.uimanager.ViewManager;
import java.util.List;
public class NativeMessagePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext context) {
return List.of(new NativeMessageModule(context));
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext context) {
return List.of();
}
}
💡 Why needed: This is how you expose your native module to React Native.
5. 🏠 MainApplication.java
✅ File: MainApplication.java
📍 Path:
android/app/src/main/java/com/sampleapp/MainApplication.java
import com.turbomodules.NativeMessagePackage #Replace this with your packagename
add(NativeMessagePackage())
6. ✅ Step-by-Step iOS Example
With the Android module complete, let’s build the iOS side of our NativeMessageModule
. We'll use Objective-C++ with TurboModules and Codegen to enable seamless native-JS communication.
📁 Step 1: Open the Xcode Workspace
Navigate to the ios
folder and open your project workspace in Xcode:
cd ios
open TurboModuleExample.xcworkspace
📦 Step 2: Create a New Group
In Xcode:
- Right-click your app folder (e.g.,
TurboModuleExample
) - Select New Group
- Name it:
NativeMessageModule
✅ This keeps native modules organized by feature.
🧱 Step 3: Add a New Cocoa Touch Class
Now add the module class file:
- Right-click NativeMessageModule group
- Select New File from Templetes...
Step 4: Choose Cocoa Touch Class
⚙️ Step 5: Name the Cocoa Touch Class NativeMessageModule
with the Objective-C language.
Note:- Rename
NativeMessageModule.m
→NativeMessageModule.mm
making it an Objective-C++ file.
Final file Structure
Here's what your folder structure should look like:
✅ Configure the Bridging Header
To ensure proper bridging between Swift and Objective-C:
- Go to Build Settings in your Xcode project.
- Search for Objective-C Bridging Header.
- Set the path relative to your project like this:
🧬 Step 6: Implement the Native Module
Update the content of both NativeMessageModule.h
and NativeMessageModule.mm
as follows:
📄 NativeMessageModule.h
//
// NativeMessageModule.h
// newArchitectureBridge
//
// Created by Amit Kumar on 21/06/25.
//
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
NS_ASSUME_NONNULL_BEGIN
@interface NativeMessageModule : RCTEventEmitter <RCTBridgeModule>
@end
NS_ASSUME_NONNULL_END
⚙️ NativeMessageModule.mm
Now update NativeMessageModule.mm
//
// NativeMessageModule.mm
// newArchitectureBridge
//
// Created by Amit Kumar on 21/06/25.
//
#import "NativeMessageModule.h"
@implementation NativeMessageModule {
BOOL hasListeners;
}
RCT_EXPORT_MODULE();
+ (BOOL)requiresMainQueueSetup {
return YES;
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"onTimerTick"];
}
// Called when the first JS listener is added
- (void)startObserving {
hasListeners = YES;
}
// Called when the last JS listener is removed
- (void)stopObserving {
hasListeners = NO;
}
RCT_EXPORT_METHOD(sendMessage:(NSString *)message) {
NSLog(@"[iOS] Received from JS: %@", message);
}
RCT_EXPORT_METHOD(getMessage:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSString *response = @"Hello from iOS";
NSLog(@"[iOS] Returning to JS (Promise): %@", response);
resolve(response);
}
RCT_EXPORT_METHOD(sendWithCallback:(RCTResponseSenderBlock)callback) {
NSString *response = @"Hello from iOS";
NSLog(@"[iOS] Returning to JS (Callback): %@", response);
callback(@[response]);
}
RCT_EXPORT_METHOD(startSendingEvents) {
if (!hasListeners) {
NSLog(@"[iOS] No listeners, not sending events");
return;
}
for (int i = 1; i <= 8; i++) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
if (!hasListeners) return;
NSString *msg = [NSString stringWithFormat:@"Event at %f", [[NSDate date] timeIntervalSince1970]];
[self sendEventWithName:@"onTimerTick" body:@{@"message": msg}];
});
}
}
@end
7. 🧪 JS Test: App.js
import React, {useState, useEffect} from 'react';
import {
View,
Button,
Text,
StyleSheet,
NativeEventEmitter,
NativeModules,
} from 'react-native';
import NativeMessageModule from './specs/NativeMessageModule';
const App = () => {
const [receivedMessage, setReceivedMessage] = useState('');
const [lastSentMessage, setLastSentMessage] = useState('');
const [callbackMsg, setCallbackMsg] = useState('');
const [eventMsg, setEventMsg] = useState('');
useEffect(() => {
const eventEmitter = new NativeEventEmitter(
NativeMessageModule,
);
const subscription = eventEmitter.addListener('onTimerTick', event => {
console.log('Event received:', event.message);
setEventMsg(event.message);
});
return () => subscription?.remove();
}, []);
const startListening = () => {
NativeMessageModule.startSendingEvents();
};
const sendMessageToNative = () => {
const message = 'Hello from React Native';
NativeMessageModule.sendMessage(message);
setLastSentMessage(message);
};
const getMessageFromNative = () => {
try {
const message = NativeMessageModule.getMessage();
setReceivedMessage(message);
} catch (error) {
console.error(error);
}
};
const useCallbackMessage = () => {
NativeMessageModule.sendWithCallback(msg => {
setCallbackMsg(msg);
});
};
return (
<View style={styles.container}>
<Text style={styles.title}>Bridging Example</Text>
<View style={styles.buttonContainer}>
<Button title="Send Message to Native" onPress={sendMessageToNative} />
<Text style={styles.message}>Last sent: {lastSentMessage}</Text>
</View>
<View style={styles.buttonContainer}>
<Button
title="Get Message from Native"
onPress={getMessageFromNative}
/>
<Text style={styles.message}>Received: {receivedMessage}</Text>
</View>
<View>
<Button title="Use Callback" onPress={useCallbackMessage} />
<Text>Received: {callbackMsg}</Text>
</View>
<View>
<Button title="Trigger Native Event" onPress={startListening} />
<Text>Received: {eventMsg}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {flex: 1, justifyContent: 'center', padding: 16},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 32,
textAlign: 'center',
},
buttonContainer: {marginBottom: 24},
message: {marginTop: 8, fontSize: 14, color: '#666'},
});
export default App;
Final Output
🎯 Conclusion
With TurboModules and Codegen, React Native’s new architecture simplifies bridging and boosts performance:
- No more boilerplate for native interfaces
- Faster & safer communication
- Clear folder structure for maintainability
Whether you're migrating from the legacy bridge or starting fresh, this setup gives you everything you need to build efficient, modern native modules.
Top comments (0)