How JavaScript talks to native code is one of the most important parts of React Native. For a long time, it was also the most confusing.
In the old days, getting JavaScript to talk to iOS or Android felt like a scene from the movie Bridge of Spies: two sides meeting on a cold, foggy bridge at midnight to trade secret messages.
In this post, we will look at how the old bridge worked, why it was slow, how the new JSI architecture replaced it with a direct hotline, and what “glue code” actually means.
React Native separates JavaScript code from native platform code. They run in different places and on different threads:
Even with the new architecture, one basic rule stays the same:
JavaScript runs inside a JS engine (Hermes or JavaScriptCore).
The JS engine does not automatically know how to call your C++, Swift, Objective-C, Java, or Kotlin code. We have to build that bridge ourselves.
So when you write:
Camera.takePicture();how does that command actually reach the real camera on your phone? Let's start with how it used to work.
The classic React Native Bridge worked like a message delivery service.
When JavaScript called a native method, it could not call native code directly. Instead, it had to turn the call into a serialized JSON-like string, put it into a queue, and send it over the bridge in batches.
You call Camera.takePicture(). JavaScript serializes the module name, method, and arguments into a JSON-like message.
Batches messages together and pushes them across the boundary to the native thread asynchronously.
Native code decodes the arguments, locates the camera module, and fires the actual camera hardware.
When the native code finished, it sent the result back the same way: turning data into a message and sending it across the bridge to JavaScript.
const width = Screen.getWidth(). Everything had to use promises or callbacks.Summary of the Old Way
The old bridge treated communication between JS and Native as sending text messages back and forth.
The foundation of the new architecture is JSI (JavaScript Interface).
JSI is a lightweight C++ API that gives React Native a direct handle on the JavaScript engine. It is not the entire module system by itself, but the C++ engine interface that makes direct communication possible.
Instead of serializing everything into a queue, native C++ objects are exposed directly to the JavaScript runtime as native-backed HostObjects.
Strictly asynchronous with JSON parsing overhead
Direct function calls with native type conversion
JSI provides C++ wrappers for JavaScript values, objects, and functions:
jsi::Value — Any JS value (number, string, boolean, etc.)jsi::Object — A JS objectjsi::Function — A JS functionjsi::Runtime — The JS engine runtimeWhen JavaScript calls a JSI function, it calls native C++ code directly in memory.
Type Conversion & Threading Reality
While JSON string parsing is eliminated, JSI still performs type marshaling between JS values and C++ types. Also, while JSI allows synchronous execution, heavy background operations (camera capture, SQLite, networking) are still dispatched to native worker threads so the JS thread never blocks.
jsi:: Mean?In C++, :: is just a way to say “look inside this folder or group”.
React Native groups its JSI types under the facebook::jsi namespace:
jsi::Value means: “The Value type inside the jsi group.”jsi::PropNameID::forAscii(rt, "multiply") uses :: because it is a static helper function.name.utf8(rt) uses . because name is an existing object instance.A great way to understand JSI is through a HostObject.
A HostObject is a C++ object that looks and acts like a normal JavaScript object. When JavaScript tries to read a property from it, the JS engine calls your C++ get() method.
class HostObject {
public:
virtual jsi::Value get(
jsi::Runtime& rt,
const jsi::PropNameID& name);
virtual void set(
jsi::Runtime& rt,
const jsi::PropNameID& name,
const jsi::Value& value);
virtual std::vector<jsi::PropNameID>
getPropertyNames(jsi::Runtime& rt);
};Let's write a native math helper that gives JS a multiply(a, b) function:
class MathHostObject : public jsi::HostObject {
public:
jsi::Value get(
jsi::Runtime& rt,
const jsi::PropNameID& name) override {
auto propName = name.utf8(rt);
if (propName == "multiply") {
// Return a C++ function to JavaScript
return jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, "multiply"),
2, // Takes 2 arguments
[](jsi::Runtime& rt,
const jsi::Value& thisVal,
const jsi::Value* args,
size_t count) -> jsi::Value {
double a = args[0].asNumber();
double b = args[1].asNumber();
return jsi::Value(a * b);
}
);
}
return jsi::Value::undefined();
}
std::vector<jsi::PropNameID>
getPropertyNames(jsi::Runtime& rt) override {
std::vector<jsi::PropNameID> names;
names.push_back(
jsi::PropNameID::forAscii(rt, "multiply")
);
return names;
}
};Next, we register it on the JavaScript global object:
void installMathModule(jsi::Runtime& runtime) {
auto hostObject = std::make_shared<MathHostObject>();
jsi::Object jsObject =
jsi::Object::createFromHostObject(runtime, hostObject);
runtime.global().setProperty(
runtime,
"mathModule",
jsObject);
}Now in JavaScript, you can call it immediately:
const result = global.mathModule.multiply(6, 7);
console.log(result); // 42 (Runs synchronously in C++!)global.mathModule.multiply(6, 7)HostObject::get("multiply")Computes 6 * 7 and returns 42 directly to JavaScript without queue delays.
No message queue. No JSON conversion. Just a direct function call.
Writing C++ by hand like this works, but it takes too much time for a real app.
Imagine having to write args[0].asNumber(), check argument counts, and handle errors manually for 50 different methods. It would be easy to make mistakes.
This is why React Native created TurboModules and Codegen.
Instead of writing all the C++ wrapper code by hand, you write a simple TypeScript specification:
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
multiply(a: number, b: number): number;
}
export default TurboModuleRegistry.getEnforcing<Spec>('MathModule');This file defines the contract: “MathModule has a multiply method that takes two numbers and returns a number.”
When you build your app, Codegen automatically reads this TypeScript file and generates all the repetitive C++ code for you:
Now your native implementation only needs to care about the real logic:
class NativeMathModule : public NativeMathModuleSpecJSI {
public:
jsi::Value multiply(jsi::Runtime& rt, double a, double b) {
return jsi::Value(a * b);
}
};Under the hood, Codegen outputs C++ JSI bindings. On iOS, these connect through Objective-C++ protocols to your Swift or Objective-C code. On Android, they connect through JNI (Java Native Interface) to your Kotlin or Java code.
These three tools work together, but they do different jobs:
| Name | What It Is | Simple Analogy |
|---|---|---|
| JSI | Low-level C++ runtime interface | The direct phone line between JS and C++ |
| TurboModules | Native module system and lifecycle manager | The manager that creates and finds native modules |
| Codegen | Build tool that writes glue code automatically | The automated translator connecting TS to Native |
In the old architecture, all native modules were loaded immediately when the app started.
TurboModules are lazy by default:
TurboModuleRegistry.getEnforcing('PaymentModule');The native module is not created until the exact moment your app calls it.
JSI is used for more than just native modules. Fabric is React Native's modern rendering engine:
Because the UI layout tree lives in C++ and communicates via JSI, React can perform synchronous layout measurement (like text sizing before paint) and enable Concurrent React features without layout jumps.
Hardware & device features (Camera, Bluetooth, Storage)
C++ Shadow Tree & synchronous UI layout rendering
The mystery behind the bridge is gone. Instead of trading slow messages across a border, React Native now has a direct, type-safe connection between JavaScript and your phone's native code.
→ Found this useful? Share it with a fellow developer 🚀