You’ve upgraded to React Native 0.76 or 0.77 to leverage the performance gains of the New Architecture. Suddenly, your Metro bundler is spamming (NOBRIDGE) logs, and your application crashes on launch—or worse, silently fails when invoking a specific third-party library.
This is the friction point of the 2025 migration: Bridgeless Mode is now enabled by default. While Fabric (rendering) and TurboModules (native logic) are the future, the ecosystem has a long tail of legacy libraries that still expect the old asynchronous JSON bridge to exist.
Here is how to diagnose the root cause and technically resolve these compatibility issues without abandoning the New Architecture.
The Root Cause: Bridgeless Mode vs. Legacy Context
To fix the crash, you must understand what changed in the runtime.
- The Old World: React Native communicated via a C++ "Bridge" that serialized JSON messages asynchronously. Native modules extended
ReactContextBaseJavaModule(Android) orRCTBridgeModule(iOS) and relied on theReactBridgeinstance to access the UI manager or other modules. - The New World (Bridgeless): The Bridge is removed. JavaScript interacts directly with Native code via JSI (JavaScript Interface).
- The Crash: Legacy libraries often attempt to access
reactContext.getCatalystInstance()(Android) or[RCTBridge currentBridge](iOS). In Bridgeless mode, these instances are null. When a library tries to invoke a method on a null bridge, the app terminates immediately (SIGABRT/CrashLoop).
The (NOBRIDGE) log in Metro is a status indicator confirming you are running in this new mode. If you see this log followed by a crash, a library is violating the Bridgeless contract.
The Solution: Configuring the Interop Layer
You do not need to rewrite third-party libraries in C++. React Native ships with an Interop Layer that wraps legacy native modules in a TurboModule shell, allowing them to communicate with the new JSI runtime.
However, auto-detection fails for complex modules. You must explicitly configure the build system to treat these specific libraries as legacy modules needing the Interop shim.
Solution 1: Expo (CNG / Prebuild)
If you are using Expo with Continuous Native Generation, you cannot edit native files directly. You must inject the configuration via expo-build-properties in your app.json.
Prerequisites:
npx expo install expo-build-properties
Configuration (app.json):
In this example, we assume react-native-legacy-lib is the crashing package.
{
"expo": {
"plugins": [
[
"expo-build-properties",
{
"android": {
"unstable_networkInspector": true,
"newArchEnabled": true
},
"ios": {
"newArchEnabled": true
}
}
]
]
}
}
Note: As of RN 0.76, simply enabling newArchEnabled usually activates the auto-interop. If a specific library fails, you must disable Bridgeless mode solely for that library or globally while keeping Fabric enabled.
Solution 2: The "Escape Hatch" (Disable Bridgeless, Keep New Arch)
If a library relies heavily on the Bridge instance (e.g., complex analytics SDKs that hook into the bridge lifecycle events), the Interop layer may not suffice.
You can run Fabric (New Renderer) and TurboModules while keeping the legacy Bridge alive for backward compatibility. This removes the (NOBRIDGE) status but keeps the performance benefits of Fabric.
Android (android/app/src/main/java/com/yourname/MainApplication.kt)
In RN 0.76+, the NewArchitectureConfig controls this. Override the isBridgelessEnabled property.
package com.yourcompany.app
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Add manually linked packages here if necessary
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
// 1. Keep New Architecture enabled
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
// 2. EXPLICITLY disable Bridgeless mode to prevent legacy crashes
override val isBridgelessEnabled: Boolean = false
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// Load the native entry point
load()
}
}
}
iOS (ios/MyApp/AppDelegate.mm)
Modify the bridgelessEnabled return value.
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"MyApp";
self.initialProps = @{};
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self getBundleURL];
}
- (NSURL *)getBundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
/// MARK: - New Architecture Configuration
// Enable the New Architecture
- (BOOL)newArchEnabled
{
return YES;
}
// DISABLE Bridgeless mode explicitly here
- (BOOL)bridgelessEnabled
{
return NO;
}
@end
Solution 3: The react-native.config.js Override (Bare Workflow)
If you are maintaining the New Architecture and Bridgeless mode (preferred), but a specific library is not being picked up by the auto-interop layer, you can force it in your root configuration.
react-native.config.js:
/**
* @type {import('@react-native-community/cli-types').Config}
*/
module.exports = {
project: {
ios: {
automaticPodsInstallation: true,
},
android: {},
},
dependencies: {
// Identify the problematic library
'react-native-legacy-analytics': {
platforms: {
android: {
// Force the CLI to generate a cmake integration for this module
libraryName: 'legacyAnalytics',
componentDescriptors: [],
androidMkPath: null,
},
ios: {
// Explicitly define this as a module needing interop
interopLayerEnabled: true
}
},
},
},
};
Note: You must run pod install (iOS) or ./gradlew clean (Android) after changing this configuration.
Why This Works
When you set bridgelessEnabled to false (Solution 2), React Native initializes the runtime in a hybrid state:
- Fabric still handles the UI rendering via the Shadow Tree and JSI.
- TurboModules are still loaded lazily.
- However, the RCTBridge (iOS) and CatalystInstance (Android) are instantiated alongside the new runtime.
This allows legacy code calling [bridge moduleForName:...] to succeed because the bridge object actually exists in memory.
When you use the Interop Layer (Solution 3 or Default), the system generates a C++ TurboModule that acts as a proxy. When JavaScript calls a function, the Interop layer intercepts the JSI call, converts the JSI values to folly::dynamic (or WriteableMap), and invokes the underlying legacy Objective-C/Java method. This allows you to stay fully "Bridgeless" while supporting older libraries.
Conclusion
The (NOBRIDGE) log is the new normal. Do not fear it; it means your app is running on the modern, high-performance infrastructure React Native has promised for years.
If you encounter crashes:
- Isolate the crashing library via stack traces.
- Attempt to force the Interop layer via
react-native.config.js. - If that fails, disable Bridgeless mode in
AppDelegateandMainApplicationwhile keepingNewArchEnabledtrue.
This hybrid approach allows you to stabilize your production build today while waiting for the ecosystem to fully catch up to the 0.76+ architecture.