Skip to main content

Migrating to React Native Bridgeless Mode: Fixing Incompatible TurboModules

 The transition to React Native’s New Architecture is no longer experimental; it is the standard. With the release of version 0.74+, Bridgeless Mode is enabled by default. This mode completely dismantles the legacy React Native Bridge, relying exclusively on JSI (JavaScript Interface) for communication between the JavaScript realm and Native code.

While this results in faster startup times and synchronous native execution, it introduces a critical blocking point: legacy native libraries.

When you flip newArchEnabled=true, you may encounter immediate crashes with errors resembling Invariant Violation: TurboModuleRegistry.getEnforcing(...): 'X' could not be found. This indicates a native module failed to register via the TurboModule system, and because the Bridge is gone, the fallback mechanism failed.

This guide details how to diagnose these failures and implements two solutions: configuring the Interop Layer for third-party libraries and migrating custom native modules to C++ TurboModules.

The Root Cause: Why Legacy Modules Break

To understand the fix, you must understand the architectural shift.

The Old Architecture (The Bridge)

In the legacy system, communication was asynchronous and serialized. When you called a native method, React Native serialized the arguments to JSON, sent them across the Bridge, and the native side deserialized them. The RCTBridgeModule (iOS) and ReactContextBaseJavaModule (Android) handled this automatically.

The New Architecture (Bridgeless)

Bridgeless mode removes the RCTBridge. Instead, it uses TurboModules.

  1. Lazy Loading: Modules are loaded only when requested.
  2. JSI (JavaScript Interface): JavaScript holds a direct reference to a C++ HostObject. Invoking a function on this object calls C++ code directly, without serialization overhead.

When Bridgeless mode is active, the runtime expects modules to expose a specific JSI configuration. If a library relies solely on the old ReactContextBaseJavaModule without the necessary C++ glue code or codegen specifications, the JavaScript layer cannot "see" the native module.

Solution 1: Forcing the Interop Layer (The Quick Fix)

React Native introduced an Interop Layer to allow legacy modules to function in the New Architecture. This layer creates a wrapper that mimics a TurboModule but routes calls through a localized, shimmed bridge.

While often automatic, the Interop Layer frequently fails to auto-detect modules that use non-standard naming conventions or complex build setups. You must explicitly register them.

Step 1: Identify the Failing Library

Check your Logcat (Android) or Xcode Console (iOS). Look for the specific module name mentioned in the Uncaught Error or Invariant Violation.

Step 2: Configure react-native.config.js

Create or update react-native.config.js in your project root. You will explicitly tell the CLI to treat specific libraries as legacy modules requiring the interop shim.

// react-native.config.js
module.exports = {
  project: {
    ios: {
      automaticPodsInstallation: true,
    },
    android: {},
  },
  dependencies: {
    // Replace 'react-native-legacy-lib' with the actual package name
    'react-native-legacy-lib': {
      platforms: {
        android: {
          // Explicitly enable the interop layer for this dependency
          componentDescriptors: [],
          cmakeListsPath: "../node_modules/react-native-legacy-lib/android/src/main/jni/CMakeLists.txt",
        },
        ios: {
          // Sometimes required to prevent codegen from running on non-compliant libs
          scriptPhases: [], 
        },
      },
    },
  },
};

Note: In React Native 0.74+, the framework attempts to auto-detect this. However, adding it manually forces the build system to generate the necessary glue code during the pod install or Gradle sync phase.

Solution 2: Migrating Custom Code to C++ TurboModules

If the Interop Layer fails, or if you need the performance benefits of JSI (e.g., for image processing or heavy calculation), you must migrate the module. We will implement a C++ TurboModule.

This approach allows you to write the logic once in C++ and share it across Android and iOS, bypassing the Java/Obj-C layer almost entirely.

Prerequisites

Ensure your project is configured for Codegen.

  1. TypeScript enabled.
  2. New Architecture enabled in android/gradle.properties (newArchEnabled=true) and ios/Podfile.

Step 1: Define the Specification

Create a generic specification file. React Native Codegen uses this interface to generate the C++ scaffolds.

File: src/specs/NativeSecureHash.ts

import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  // Define a synchronous method for high performance
  hashString(input: string): string;
}

// The name 'SecureHash' must match the native module name exactly
export default TurboModuleRegistry.getEnforcing<Spec>('SecureHash');

Step 2: Configure the Package (package.json)

You must tell Codegen where to find this spec and what to name the generated C++ library.

{
  "name": "my-app",
  "codegenConfig": {
    "name": "RNSecureHashSpec",
    "type": "modules",
    "jsSrcsDir": "src/specs",
    "android": {
      "javaPackageName": "com.myapp.modules"
    }
  }
}

Run codegen manually to ensure files are created: yarn react-native codegen

Step 3: The C++ Implementation

This is the core of the Bridgeless migration. Instead of writing Java or Obj-C, we write C++. This code sits in android/app/src/main/cpp/ or a shared cpp folder.

Header File: NativeSecureHash.h

#pragma once

#if __has_include(<ReactCommon/TurboModule.h>)
#include <ReactCommon/TurboModule.h>
#else
#include <ReactCommon/TurboModule.h>
#endif

#include "RNSecureHashSpecJSI.h" // Generated by Codegen

namespace facebook::react {

class NativeSecureHash : public NativeSecureHashCxxSpec<NativeSecureHash> {
public:
  NativeSecureHash(std::shared_ptr<CallInvoker> jsInvoker);

  // Method implementation matching the TS spec
  std::string hashString(jsi::Runtime &rt, std::string input) override;
};

} // namespace facebook::react

Source File: NativeSecureHash.cpp

#include "NativeSecureHash.h"
#include <functional>

namespace facebook::react {

NativeSecureHash::NativeSecureHash(std::shared_ptr<CallInvoker> jsInvoker)
    : NativeSecureHashCxxSpec(std::move(jsInvoker)) {}

std::string NativeSecureHash::hashString(jsi::Runtime &rt, std::string input) {
    // Perform high-performance logic here. 
    // This runs synchronously on the JS thread if invoked synchronously.
    
    // Example: Simple obfuscation (Replace with actual C++ hashing lib)
    std::string hashed = "hashed_" + input; 
    return hashed;
}

} // namespace facebook::react

Step 4: Registering the Module (Android)

You need to connect this C++ class to the Android runtime so React Native can discover it.

Modify android/app/src/main/jni/OnLoad.cpp (create it if it doesn't exist, usually located alongside your MainApplicationModuleProvider).

#include <ReactCommon/CxxTurboModuleUtils.h>
#include "NativeSecureHash.h"

// ... inside the module provider registration ...

std::shared_ptr<TurboModule> moduleProvider(const std::string &name, const std::shared_ptr<CallInvoker> &jsInvoker) {
  if (name == "SecureHash") {
    return std::make_shared<facebook::react::NativeSecureHash>(jsInvoker);
  }
  return nullptr;
}

Deep Dive: Why This Fix Works

The migration to C++ TurboModules solves the "Bridgeless" crash by adhering to the JSI Protocol.

In the Old Architecture, the JS thread generated a message queue. The Bridge processed this queue asynchronously. If the native module wasn't initialized, the message sat in limbo or caused a timeout.

In the New Architecture with the code above:

  1. HostObject: The NativeSecureHash class inherits from NativeSecureHashCxxSpec. Codegen ensures this class exposes a get() method compliant with the JavaScript Interface (JSI).
  2. Runtime injection: When JavaScript calls TurboModuleRegistry.get('SecureHash'), the runtime looks up the C++ map registered in OnLoad.cpp.
  3. Direct Memory Access: The jsi::Runtime &rt argument in your C++ function allows the native code to read JavaScript strings directly from memory without serializing them to JSON. This bypasses the need for the legacy bridge entirely.

Common Pitfalls and Edge Cases

1. Codegen Caching

The most common issue during migration is stale Codegen artifacts. If you change the TypeScript spec, the C++ header files generated in node_modules or build folders might not update.

  • Fix: Always run cd android && ./gradlew clean and rm -rf ios/build after modifying .ts specs.

2. Thread Safety

In Bridgeless mode, TurboModules are often invoked synchronously on the JavaScript thread.

  • Risk: If your C++ implementation performs heavy I/O (file writing, network requests), you will freeze the UI.
  • Solution: For heavy tasks, use the CallInvoker to dispatch work to a background thread and invoke a callback or Promise (via jsi::Function) when complete.

3. Java/Obj-C Dependencies

If your C++ module needs to call an existing Java or Swift library, you need JNI (Java Native Interface) or Objective-C++ (.mm files). This adds complexity. If the library is purely native logic (math, compression, hashing), keep it in pure C++ to maximize portability and performance.

Conclusion

Migrating to Bridgeless mode is mandatory for future-proofing your React Native application. While the Interop Layer serves as a functional bandage for legacy libraries, writing C++ TurboModules is the definitive solution for high-performance, stable native integration. It eliminates the serialization bottleneck and aligns your codebase with the modern React Native ecosystem.