The React Native Bridge is a silent, asynchronous messenger that allows your JavaScript code to talk to native modules and vice-versa, but its real magic lies in how it serializes and deserializes data for every single inter-process communication.

Let’s watch it in action. Imagine you have a simple native module that exposes a method addNumbers to JavaScript.

Native Code (Example: Objective-C)

// MyNativeModule.m
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface RCT_EXTERN_MODULE(MyNativeModule, NSObject)

RCT_EXTERN_METHOD(addNumbers:(NSInteger)a
                  withB:(NSInteger)b
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

@end

@implementation MyNativeModule

RCT_EXPORT_MODULE();

- (void)addNumbers:(NSInteger)a withB:(NSInteger)b resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
  if (a < 0 || b < 0) {
    NSError *error = [NSError errorWithDomain:@"com.example.mynativemodule" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Numbers must be non-negative"}];
    reject(@"invalid_input", @"Numbers must be non-negative", error);
  } else {
    resolve(@(a + b));
  }
}

@end

JavaScript Code

import { NativeModules } from 'react-native';
const { MyNativeModule } = NativeModules;

async function performAddition(num1, num2) {
  try {
    const sum = await MyNativeModule.addNumbers(num1, num2);
    console.log(`The sum is: ${sum}`);
  } catch (error) {
    console.error(`Error adding numbers: ${error.message}`);
  }
}

performAddition(5, 10); // Output: The sum is: 15
performAddition(-5, 10); // Output: Error adding numbers: Numbers must be non-negative

When MyNativeModule.addNumbers(5, 10) is called from JavaScript, here’s what happens under the hood:

  1. JS Thread -> Bridge: The JavaScript engine (JSC or Hermes) serializes the method name ("addNumbers") and its arguments (5, 10) into a JSON-like structure. This serialization happens on the JS thread.
  2. Bridge -> Native Thread: This serialized data is then passed to the bridge. The bridge queues this request. When the native thread is ready, it dequeues the request.
  3. Native Thread Execution: The bridge deserializes the arguments and invokes the corresponding native method (addNumbers:withB:resolver:rejecter:) on the native thread.
  4. Native Operation: The native method executes, performing the addition.
  5. Native Thread -> Bridge: If the operation is successful, the result (15) is serialized by the bridge. If there’s an error, the error details are serialized.
  6. Bridge -> JS Thread: The serialized result/error is sent back to the JS thread.
  7. JS Thread Deserialization: The JS thread deserializes the data and resolves or rejects the promise that was returned to the JavaScript code.

The bridge is essentially a multiplexer and serializer. It takes calls from JS, packages them up, sends them to the native side, and then takes results from native, packages them up, and sends them back to JS. This happens over a shared memory buffer or through message queues, depending on the specific implementation and React Native version.

The core problem the bridge solves is the fundamental difference between JavaScript’s event loop and the native platform’s threading model. JavaScript runs in a single thread, while native platforms use multiple threads for UI, background tasks, etc. The bridge acts as the intermediary to manage this cross-thread communication safely and efficiently.

You control the communication by exporting native modules and methods using RCT_EXPORT_MODULE() and RCT_EXTERN_METHOD() (or their equivalents in Java/Kotlin for Android), and by calling these exported methods from your JavaScript code via NativeModules. For asynchronous operations, you use RCTPromiseResolveBlock and RCTPromiseRejectBlock to send results back to JS. For events that don’t originate from a direct JS call (e.g., a native sensor reading), you’d use RCTEventEmitter.

What most people don’t realize is that the bridge has a significant performance cost. Every single piece of data passed across the bridge—method names, arguments, return values—is serialized into JSON (or a similar format) and then deserialized on the other side. For large amounts of data or frequent calls, this serialization/deserialization overhead can become a bottleneck, leading to dropped frames or sluggish UI. The bridge is designed to be efficient, but it’s not free.

The next major concept you’ll encounter is how to optimize this communication, particularly by minimizing bridge calls and data transfer, or by exploring newer architectures like TurboModules.

Want structured learning?

Take the full React course →