Phase 2: React Native & Cross-Platform

TurboModules & JSI for synchronous native calls

Advanced ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you're a super chef in a busy kitchen – that's your app! You're brilliant at making delicious meals (your app's main code), but sometimes you need special ingredients from a super-secret, locked pantry. This pantry holds your phone's powerful built-in features, like the camera or GPS, which speak a different "language" than your main code. The old way to get these ingredients was to write a note, give it to a messenger (the "JavaScript Bridge"), who would run to the pantry, find the ingredient, package it carefully, and bring it back. This took time, like waiting for a slow delivery, especially if you needed lots of things quickly. It was functional, but definitely not instant!

Now, imagine you have a secret, direct tunnel from your kitchen right into that pantry! No more messengers, no more notes, or special packaging. You can just reach your hand in and grab exactly what you need, instantly. That secret tunnel is what we call the JSI (JavaScript Interface). It's like a super-fast connection that lets you, the chef, talk directly to the pantry. Your hands (the app's main code) can literally touch and use the ingredients (the phone's special features) without any middlemen. This makes things much, much faster because there's no waiting or translating. It's like you can instantly grab a banana or turn on the blender without any delay, making your cooking much more efficient.

Building on this super-fast tunnel, we have something called TurboModules. Think of these as special, pre-made ingredient kits perfectly organized inside the pantry, right at the end of your tunnel. You only grab a kit when you actually need it, like a "banana smoothie kit" that has everything ready. This is like "lazy loading" – getting things only when you need them, saving time at the start of your day. These kits also come with super clear labels, like a recipe card, so you never accidentally grab sugar when you meant salt. This "type safety" means fewer mistakes! So, when you build an app that needs to quickly switch camera modes or constantly update your location, using these TurboModules with the JSI tunnel means everything happens instantly, without any lag. It's like your chef can whip up amazing meals faster than ever, keeping everyone happy and well-fed!

Traditional React Native applications relied on the JavaScript Bridge for communication between the JavaScript realm and native host platforms (iOS/Android). While functional, this bridge introduced overhead through asynchronous JSON serialization and deserialization, leading to performance bottlenecks and latency, especially during complex interactions or frequent calls. The JavaScript Interface (JSI) directly addresses this. JSI is a lightweight C++ layer that allows the JavaScript runtime (like Hermes or JSC) to directly hold references to C++ host objects and invoke their methods. This fundamental shift bypasses the bridge entirely, establishing a direct, synchronous communication channel that’s significantly faster and more efficient.

TurboModules are the next evolution of React Native's Native Modules, built upon the power of JSI. They were designed to overcome the limitations of the old bridge by leveraging JSI's direct access capabilities. Key benefits of TurboModules include lazy loading (modules are loaded only when needed, improving app startup time), type safety (achieved through code generation that creates strongly typed interfaces), and, critically, the ability to perform synchronous native calls. With JSI, a JavaScript function can directly call an exposed C++ method and immediately receive its return value, much like calling another JavaScript function. This eliminates the need for Promises or callbacks for operations that demand instant results.

Practically, synchronous native calls are invaluable for quick, non-blocking operations where an immediate result is crucial and the overhead of an asynchronous roundtrip would be detrimental. Think about querying simple system states like the current UI theme, checking a specific hardware capability, or performing a very fast calculation directly on the native side. However, a significant caveat applies: synchronous calls block the JavaScript thread. Prolonged or complex synchronous operations will freeze your UI, leading to a poor user experience. Therefore, they must be used sparingly and only for operations guaranteed to complete almost instantly. For longer-running tasks, network requests, or I/O operations, asynchronous patterns (Promises, callbacks) remain the best practice to keep your UI responsive and fluid.

Key Takeaways

  • JSI enables direct C++-JavaScript communication, bypassing the old bridge entirely.
  • TurboModules leverage JSI for enhanced performance, lazy loading, type safety, and synchronous native calls.
  • Synchronous native calls block the JavaScript thread; use them sparingly for quick, non-UI-blocking operations.
  • Most native operations should remain asynchronous to ensure a responsive and smooth user interface.

Code Example

typescript
Preview

How this code works

This code demonstrates how React Native applications interact with platform-specific native code (e.g., Swift on iOS or Kotlin on Android) using TurboModules and JSI, specifically illustrating both synchronous and asynchronous approaches for calling native functions. The code first defines an interface, MySystemInfoType, to clearly specify the methods available on a native module called MySystemInfo. This module is then accessed via NativeModules.MySystemInfo, with its type asserted to ensure TypeScript correctly understands its capabilities.

The MySystemInfo.getThemeSync() call demonstrates a synchronous native operation. This means the JavaScript code execution pauses until the native method completes and returns a value, like 'light' or 'dark', directly. A subtle but critical detail for beginners is that synchronous native calls can throw JavaScript errors directly if the native side encounters an issue, making the try...catch block essential for robust error handling. In contrast, MySystemInfo.getDeviceId() is an asynchronous native call. It immediately returns a Promise, allowing JavaScript execution to continue without waiting. The actual device ID is then retrieved later using .then() once the native operation finishes, with errors handled through Promise rejections rather than direct throws.