How to Add Custom JSI Bindings in Your TurboModule (and Why You Might Need Them)

The React Native New Architecture, powered by Fabric and TurboModules, brings major improvements in performance and developer experience. For most cases, TurboModules are the go-to solution: type-safe, auto-generated bindings that eliminate boilerplate and bridge overhead. But sometimes you need more. Sometimes the abstractions don’t cover your exact use case.

That’s where the JavaScript Interface (JSI) comes in!

JSI is the low-level API that underpins TurboModules. It lets you inject custom functions directly into the JavaScript runtime, hold references to JS objects, and move data without serialization. It’s the tool you reach for when TurboModules alone aren’t enough.

Why Go Beyond TurboModules?

Here’s a real example. While working on audio transcription, we needed to pass a large chunk of binary data (an ArrayBuffer) from JavaScript into a native speech-to-text module. TurboModules didn’t yet provide direct support for ArrayBuffer. Falling back to base64 encoding wasn’t an option. It was too slow and memory-heavy. The data was already in memory; we just needed to expose it directly.

This is where custom JSI bindings shine. Instead of unnecessary conversions, they give you access to the JS engine’s memory.

The Escape Hatch: RCTTurboModuleWithJSIBindings

React Native provides a protocol exactly for these scenarios. If your TurboModule implements RCTTurboModuleWithJSIBindings, you can inject your own bindings when the module initializes.

Step 1. Declare conformance in your header

#import <ReactCommon/RCTTurboModuleWithJSIBindings.h>

@interface MyCustomModule <RCTTurboModuleWithJSIBindings>
@end

Step 2. Implement the protocol method

- (void)installJSIBindingsWithRuntime:(facebook::jsi::Runtime &)rt
                          callInvoker:(const std::shared_ptr<facebook::react::CallInvoker> &)jsInvoker {
  rt.global().setProperty(runtime, "__MyCustomGlobal", "Hello World!");
}

This gives you direct access to the JSI runtime and call invoker.

Example: Passing an ArrayBuffer to Native

With JSI, we can create a native function that directly accepts an ArrayBuffer from JavaScript without any serialization overhead. We can inject a global function, __native__processBytes__(arrayBuffer), that can access the raw data.

Here’s how you can do it, step-by-step, inside the installJSIBindingsWithRuntime method:

Step 1: Define the Host Function

First, create a JSI function from a C++ lambda. This function will be callable from JavaScript. The body of the function is where you'll add the logic to handle the ArrayBuffer.

auto processFunc = jsi::Function::createFromHostFunction(
    rt,
    jsi::PropNameID::forAscii(rt, "__native__processBytes__"),
    1, // expects one argument (arrayBuffer)
    [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
      // function body goes here
      return jsi::Value::undefined();
    });

Let's quickly break down the arguments passed to createFromHostFunction:

  • rt: This is a reference to the JSI Runtime, the execution context for the JavaScript world. You need it to create and interact with any JS values.
  • jsi::PropNameID::forAscii(...): This creates an identifier for the function's name. JSI uses this ID for faster lookups than using raw strings.
  • 1: This is the paramCount, telling the JSI runtime that your function expects one argument from JavaScript.
  • [](...) { ... }: This is the C++ lambda that acts as the Host Function. It's the native code that runs when your function is called from JavaScript. It receives its own arguments to access the runtime, the JS this context, and the arguments passed from JS.

Step 2: Validate Input and Access Raw Data

Inside the lambda, validate that the first argument is an ArrayBuffer.

if (count < 1 || !args[0].isObject() || !args[0].asObject(rt).isArrayBuffer(rt)) {
  throw jsi::JSError(rt, "First argument must be an ArrayBuffer");
}

Then, get a direct, zero-copy pointer to its underlying data.

auto arrayBuffer = args[0].asObject(rt).getArrayBuffer(rt);

const void *data = arrayBuffer.data(rt);
size_t size = arrayBuffer.size(rt);

NSLog(@"Received ArrayBuffer of size: %zu", size);

Step 3: Register the Function Globally

Finally, attach your newly created JSI function to the global JavaScript object, making it accessible as global.__native__processBytes__.

rt.global().setProperty(rt, "__native__processBytes__", std::move(processFunc));

Step 4: Call it from JavaScript

const bytes = new Uint8Array([1, 2, 3, 4]);
global.__native__processBytes__(bytes.buffer);

This demonstrates the core concept: accessing JavaScript's memory directly from native code.

For a more advanced example covering handling array buffers and returning a promise, you can refer to the code inside React Native AI.

Why This Matters

TurboModules cover most native integration needs. But when you hit an edge case, such as unsupported type or advanced optimizations, JSI bindings give you the flexibility to:

  • Expose custom global functions
  • Work with raw JS engine objects
  • Bridge async native APIs directly into Promises
  • Optimize data transfer without serialization

Array Buffers are coming to Turbo Modules

We're actively working on bringing first-class ArrayBuffer support directly to TurboModules. This edge case is very popular, so we decided to bring a solution to everyone, allowing you to use a much simpler CodeGen instead.

🎉 PSA: First-class ArrayBuffer support is coming to TurboModules!

No more base64 serialization. Massive performance wins for binary data!

For an 8 MB payload, that means:
- base64 → ~666 ms 🐢
- direct ArrayBuffer → ~1 ms 🚀

Huge 👏 to @_ikswodarap for shipping this! pic.twitter.com/OsGXqgqEtH

— Mike (@grabbou) September 8, 2025

Internally, we're huge fans of the new architecture and TurboModules and continue to be on the lookout for further improvements.

Closing Thoughts

If TurboModules are the safe, paved road, JSI is the open terrain. You rarely need it, but knowing how to reach for it unlocks the full potential of React Native’s native layer.

At Callstack, we’ve used these techniques in real-world projects where performance and low-level access matter. If you’re facing a similar edge case, let’s talk.

Insights

Learn more about Performance

cover

Jul 3Podcast

Why Nitro Is More Than a Faster Bridge

Marc Rousavy (Margelo) talks Nitro modules, native state, zero-copy data, Vision Camera, and why speed is only part of the story.

cover

May 25Article

Profile React Components in React Native Release Builds

Learn how to profile React components in React Native release builds with @callstack/inspector, using an Expo app setup that requires zero native code changes.

cover

Nov 21Article

Memory Ownership Models: When JavaScript Meets Native Code

A deep dive into how React Native handles memory when JavaScript and native code share ArrayBuffer data. Learn why async processing can break, how GC affects raw pointers, and the safe patterns for building high-performance native integrations.

cover

Oct 14Article

From Files to Buffers: Building Real-Time Audio Pipelines in React Native

Learn how to move beyond file-based audio APIs and work directly with in-memory PCM data in React Native. This guide shows how to construct WAV buffers, use JSI for zero-copy transfers, and enable real-time audio processing.

cover

Jul 24Article

Exploring Fibers: Cooperative Multitasking and Lock-Free Task Execution

Explore how fibers enable lightweight, cooperative multitasking. This guide walks through building a simple scheduler, managing task dependencies without locks, and using Tracy to visualize performance.

cover

May 21Article

Multithreading Isn’t Free: Performance Pitfalls Visualized

Learn how to profile and optimize multithreaded apps. Discover why more threads don’t always mean faster results and how bad thread contention really is.

Event Cover

Apr 28Webinar

Master JavaScript Performance in React Native (Part 2)

Learn to fix JS performance issues in React Native-from debounce bugs to Redux selectors, in this hands-on webinar with real code examples.

Events Cover

Apr 24Talk

Tips & Tricks From The Ultimate Guide to React Native Optimization 2025

View flattening, React 19 hooks, real DevTools tips-learn what actually helps your React Native app run faster.

Events Cover

Apr 24Talk

User-Focused Performance Tracking

Discover how Expensify defines and tracks performance metrics that actually impact user experience.

cover

Apr 11Article

Profiling React Native Internals with Tracy for Peak Performance

Learn how to profile React Native internals with Tracy to uncover bottlenecks, optimize performance, and gain deep insights into your app’s native behavior.

cover

Dec 18Podcast

React Native FastIO: Nitro-Powered Performance Boost

Listen to Mike Grabowski talk about React Native FastIO, a collection of primitives for working with data in React Native and its performance benefits.

cover

Oct 15Podcast

Superfast Nitro Modules With Marc Rousavy

Even faster React Native apps? That's possible with Nitro Modules! Tune in to the conversation with their creator, Marc Rousavy.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.