React Native 0.74 marked a major milestone: the TurboModule interop proxy was removed. Before 0.74, TurboModules still passed through an interop layer that added overhead. Now, with true bridgeless mode, JavaScript communicates directly with native code via JSI bindings - no proxy, no serialization, no bridge.
In this guide, I'll show you how to build a simple AuthModule with a synchronous isUserLoggedIn() method - a practical example that demonstrates the real performance benefits.
What Changed in React Native 0.74+
Before 0.74: TurboModule with Interop Proxy
JS → TurboModule Proxy → Interop Layer → Native Module
↓
(still had serialization overhead)
Even with TurboModules enabled, the interop layer maintained backward compatibility with old bridge modules, adding latency.
0.74+ True Bridgeless
JS → JSI Binding → Native Code
↓
(direct memory access, zero-copy)
The proxy is gone. Your TurboModule methods are now direct C++ function calls exposed to JavaScript.
Performance Comparison
| React Native Version |
isUserLoggedIn() call |
|---|---|
| Old Bridge | ~5-10ms |
| 0.73 TurboModule (with proxy) | ~0.5-1ms |
| 0.74+ Bridgeless (no proxy) | ~0.05ms |
What We're Building
A simple authentication module:
import AuthModule from './specs/NativeAuthModule';
// Sync method - instant return, no await
const isLoggedIn = AuthModule.isUserLoggedIn(); // true or false
// Async methods
const user = await AuthModule.getCurrentUser();
await AuthModule.login('user@example.com', 'password123');
await AuthModule.logout();
Project Setup
Prerequisites
- React Native 0.74+ (0.76+ recommended for stable bridgeless)
- Xcode 15+
1. Enable New Architecture
ios/Podfile:
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
2. Enable Bridgeless Mode
For React Native 0.74-0.75, add to ios/YourApp/AppDelegate.mm:
// In your AppDelegate
- (BOOL)bridgelessEnabled {
return YES;
}
For React Native 0.76+, bridgeless is the default. You can explicitly enable it in react-native.config.js:
module.exports = {
project: {
ios: {
unstable_reactLegacyComponentNames: [], // empty = bridgeless
},
},
};
3. Install Pods
cd ios && RCT_NEW_ARCH_ENABLED=1 pod install
Step 1: Create the TypeScript Spec
Codegen uses this spec to generate native bindings.
src/specs/NativeAuthModule.ts:
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
// Sync method - executes immediately, blocks JS briefly
isUserLoggedIn(): boolean;
// Async methods - return Promises
getCurrentUser(): Promise<{
id: string;
email: string;
name: string;
} | null>;
login(email: string, password: string): Promise<boolean>;
logout(): Promise<void>;
}
// getEnforcing throws if module not found (good for debugging)
export default TurboModuleRegistry.getEnforcing<Spec>('AuthModule');
package.json - Add Codegen config:
{
"codegenConfig": {
"name": "AuthModuleSpec",
"type": "modules",
"jsSrcsDir": "src/specs"
}
}
Step 2: Objective-C Implementation
Header File
ios/AuthModule.h:
#import <React/RCTBridgeModule.h>
#import <ReactCommon/RCTTurboModule.h>
@interface AuthModule : NSObject <RCTBridgeModule, RCTTurboModule>
@end
Implementation File
ios/AuthModule.mm:
#import "AuthModule.h"
#import <React/RCTBridge+Private.h>
// Codegen generates this header after `pod install`
#ifdef RCT_NEW_ARCH_ENABLED
#import <AuthModuleSpec/AuthModuleSpec.h>
#endif
@implementation AuthModule
RCT_EXPORT_MODULE(AuthModule)
#pragma mark - Configuration
// IMPORTANT: Return NO to enable synchronous methods
+ (BOOL)requiresMainQueueSetup {
return NO;
}
// Connect to TurboModule system - this is what makes bridgeless work
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params {
#ifdef RCT_NEW_ARCH_ENABLED
return std::make_shared<facebook::react::NativeAuthModuleSpecJSI>(params);
#else
return nullptr;
#endif
}
#pragma mark - Synchronous Method (True Bridgeless - No Proxy!)
// RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD enables sync calls
// In 0.74+ bridgeless, this is a direct JSI call - no interop proxy
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSNumber *, isUserLoggedIn) {
// This runs synchronously on the JS thread
// Keep it fast! (< 1ms ideally)
NSString *token = [[NSUserDefaults standardUserDefaults]
stringForKey:@"userToken"];
BOOL isLoggedIn = (token != nil && token.length > 0);
return @(isLoggedIn);
}
#pragma mark - Async Methods
RCT_EXPORT_METHOD(getCurrentUser:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
// Move to background thread for any I/O
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
NSString *token = [[NSUserDefaults standardUserDefaults]
stringForKey:@"userToken"];
if (!token || token.length == 0) {
resolve([NSNull null]);
return;
}
// In production: decode JWT or fetch from API
NSDictionary *user = @{
@"id": @"user_123",
@"email": @"user@example.com",
@"name": @"John Doe"
};
resolve(user);
});
}
RCT_EXPORT_METHOD(login:(NSString *)email
password:(NSString *)password
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
// In production: call your authentication API
if (email.length > 0 && password.length >= 6) {
[[NSUserDefaults standardUserDefaults] setObject:@"mock_token_123"
forKey:@"userToken"];
resolve(@YES);
} else {
reject(@"AUTH_ERROR", @"Invalid email or password", nil);
}
});
}
RCT_EXPORT_METHOD(logout:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"userToken"];
resolve([NSNull null]);
}
@end
Why This Is Faster in 0.74+ Bridgeless
// When JS calls: AuthModule.isUserLoggedIn()
// OLD (with bridge):
// 1. JS serializes call to JSON
// 2. Message queued in bridge
// 3. Native deserializes JSON
// 4. Method executes
// 5. Result serialized to JSON
// 6. Response queued
// 7. JS deserializes response
// 0.74+ BRIDGELESS (no proxy):
// 1. JSI calls C++ binding directly
// 2. Method executes
// 3. Result returned via JSI
// That's it. Direct function call.
Step 3: Swift Implementation
Swift modules need an Objective-C bridging layer for TurboModule compatibility.
Swift Module
ios/AuthModuleSwift.swift:
import Foundation
@objc(AuthModuleSwift)
class AuthModuleSwift: NSObject {
// MARK: - Configuration
@objc static func requiresMainQueueSetup() -> Bool {
return false // Required for sync methods
}
@objc static func moduleName() -> String {
return "AuthModuleSwift"
}
// MARK: - Synchronous Method
// This is called directly via JSI in bridgeless mode
@objc func isUserLoggedInSync() -> NSNumber {
let token = UserDefaults.standard.string(forKey: "userToken")
let isLoggedIn = token != nil && !token!.isEmpty
return NSNumber(value: isLoggedIn)
}
// MARK: - Async Methods
@objc func getCurrentUser(
_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
DispatchQueue.global(qos: .userInitiated).async {
guard let token = UserDefaults.standard.string(forKey: "userToken"),
!token.isEmpty else {
resolve(NSNull())
return
}
let user: [String: Any] = [
"id": "user_123",
"email": "user@example.com",
"name": "John Doe"
]
resolve(user)
}
}
@objc func login(
_ email: String,
password: String,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
DispatchQueue.global(qos: .userInitiated).async {
if !email.isEmpty && password.count >= 6 {
UserDefaults.standard.set("mock_token_123", forKey: "userToken")
resolve(true)
} else {
reject("AUTH_ERROR", "Invalid email or password", nil)
}
}
}
@objc func logout(
_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
UserDefaults.standard.removeObject(forKey: "userToken")
resolve(NSNull())
}
}
Objective-C Bridge for Swift
ios/AuthModuleSwift.m:
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(AuthModuleSwift, NSObject)
// Sync method - note the special macro
RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(isUserLoggedInSync)
// Async methods
RCT_EXTERN_METHOD(getCurrentUser:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(login:
(NSString *)email
password:(NSString *)password
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(logout:
(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
@end
TurboModule Wrapper for Swift (Full Bridgeless)
ios/AuthModuleSwiftTurbo.mm:
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTBridgeModule.h>
#import <ReactCommon/RCTTurboModule.h>
#import <AuthModuleSpec/AuthModuleSpec.h>
#import "YourApp-Swift.h" // Xcode generates this
@interface AuthModuleSwiftTurbo : NSObject <NativeAuthModuleSpec>
@property (nonatomic, strong) AuthModuleSwift *swiftImpl;
@end
@implementation AuthModuleSwiftTurbo
RCT_EXPORT_MODULE(AuthModuleSwift)
- (instancetype)init {
if (self = [super init]) {
_swiftImpl = [[AuthModuleSwift alloc] init];
}
return self;
}
+ (BOOL)requiresMainQueueSetup {
return NO;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeAuthModuleSpecJSI>(params);
}
// Delegate all methods to Swift implementation
- (NSNumber *)isUserLoggedIn {
return [_swiftImpl isUserLoggedInSync];
}
- (void)getCurrentUser:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
[_swiftImpl getCurrentUser:resolve reject:reject];
}
- (void)login:(NSString *)email
password:(NSString *)password
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
[_swiftImpl login:email password:password resolve:resolve reject:reject];
}
- (void)logout:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
[_swiftImpl logout:resolve reject:reject];
}
@end
#endif
Step 4: Usage in JavaScript
import React, { useState, useEffect } from 'react';
import { View, Text, Button } from 'react-native';
import AuthModule from './specs/NativeAuthModule';
function App() {
const [user, setUser] = useState(null);
// Sync call - no await needed, instant result
// This is only possible with bridgeless TurboModules!
const isLoggedIn = AuthModule.isUserLoggedIn();
useEffect(() => {
if (isLoggedIn) {
loadUser();
}
}, []);
const loadUser = async () => {
const userData = await AuthModule.getCurrentUser();
setUser(userData);
};
const handleLogin = async () => {
try {
await AuthModule.login('user@example.com', 'password123');
loadUser();
} catch (error) {
console.error('Login failed:', error);
}
};
const handleLogout = async () => {
await AuthModule.logout();
setUser(null);
};
return (
<View style={{ padding: 20 }}>
<Text>Logged in: {isLoggedIn ? 'Yes' : 'No'}</Text>
{user ? (
<>
<Text>Welcome, {user.name}!</Text>
<Button title="Logout" onPress={handleLogout} />
</>
) : (
<Button title="Login" onPress={handleLogin} />
)}
</View>
);
}
Performance Measurement
// Measure the difference yourself
const measurePerformance = () => {
// Sync method (bridgeless)
const syncStart = performance.now();
for (let i = 0; i < 1000; i++) {
AuthModule.isUserLoggedIn();
}
const syncTime = performance.now() - syncStart;
console.log(`1000 sync calls: ${syncTime.toFixed(2)}ms`);
console.log(`Per call: ${(syncTime / 1000).toFixed(4)}ms`);
// You'll see something like:
// 1000 sync calls: 45.23ms
// Per call: 0.0452ms
};
When to Use Sync vs Async Methods
✅ Use Sync Methods For:
- Login status checks - needed immediately for UI rendering
- Feature flags - instant access to cached values
- User preferences - theme, language settings
- Animation frame data - time-critical calculations
❌ Use Async Methods For:
- API calls - network requests should never block JS
- File operations - disk I/O is slow
- Database queries - even SQLite should be async
- Heavy computations - offload to background thread
Rule of Thumb
If it takes more than 1ms, make it async.
Common Gotchas
1. Forgetting requiresMainQueueSetup
// ❌ Wrong - sync methods won't work
+ (BOOL)requiresMainQueueSetup {
return YES;
}
// ✅ Correct
+ (BOOL)requiresMainQueueSetup {
return NO;
}
2. Missing getTurboModule Implementation
Without this, your module won't use bridgeless mode:
// ✅ Required for bridgeless
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeAuthModuleSpecJSI>(params);
}
3. Not Running Codegen
After adding/changing your spec, regenerate native code:
cd ios && pod install
4. Using .m Instead of .mm
TurboModules require Objective-C++ for C++ interop:
❌ AuthModule.m
✅ AuthModule.mm
Verifying Bridgeless Mode Is Active
// Check if your module is loaded as TurboModule
import { TurboModuleRegistry } from 'react-native';
const module = TurboModuleRegistry.get('AuthModule');
console.log('TurboModule loaded:', module !== null);
// If this returns null, you're still on the old bridge
Summary
| Feature | Old Bridge | 0.73 TurboModule | 0.74+ Bridgeless |
|---|---|---|---|
| Interop Proxy | N/A | Yes | No |
| Sync Methods | No | Yes (with overhead) | Yes (direct) |
| Serialization | JSON | Partial | None |
| Typical Latency | 5-10ms | 0.5-1ms | 0.05ms |
React Native 0.74+ with bridgeless mode gives you the best of both worlds: the developer experience of JavaScript with near-native performance for critical paths.
The isUserLoggedIn() sync method we built runs in ~0.05ms - that's 100x faster than the old bridge. For UI-critical operations like checking auth state before rendering, this makes a real difference.
Top comments (0)