How do you integrate native modules and custom bridges in React Native?
React Native Developer
answer
When React Native lacks a capability, build a native module with a typed JavaScript surface and a hardened native core. For simple asynchronous calls, expose methods via the bridge with promises or callbacks. For high-throughput and synchronous needs, use TurboModules and JSI to bypass the legacy bridge. Emit events, keep heavy work off the JavaScript thread, and validate inputs. Package with autolinking, version the API, and add tests and fallbacks so the app degrades gracefully if the module is missing.
Long Answer
Extending React Native safely means designing a clear contract between JavaScript and native code, choosing the right integration path for the job, and hardening the module for performance, lifecycle, and portability. The toolkit includes classic bridge modules, TurboModules, and JSI for zero-copy, high-performance bindings. A good integration reads as a small, typed JavaScript surface that protects a robust native core.
1) Choose the right integration model
Start with requirements. If the feature is asynchronous, relatively low-frequency, and does not require synchronous results, a classic native module is often sufficient. If you need synchronous access, large data transfer, or very frequent calls, prefer TurboModules and JSI, which remove JSON serialization overhead and the asynchronous wall of the legacy bridge. For UI components, use Fabric and view managers.
2) Define the JavaScript contract first
Design a minimal, typed API in TypeScript that reflects the domain, not the platform quirks. Prefer explicit parameter objects and discriminated unions for options. Document error codes and events. Validate inputs in JavaScript before crossing the boundary, then validate again in native code. If the feature can be absent, return a meaningful fallback or throw a typed “not available” error.
3) Classic native modules (asynchronous)
- Android (Kotlin or Java): implement ReactContextBaseJavaModule, annotate exported methods with @ReactMethod, and return results via Promise or callbacks. Use DeviceEventManagerModule.RCTDeviceEventEmitter for events. Run blocking work on a background executor and switch back for callbacks. Avoid holding a strong reference to Activity; use WeakReference and lifecycle listeners.
- iOS (Swift or Objective-C): conform to RCTBridgeModule, export with RCT_EXPORT_MODULE and RCT_EXPORT_METHOD. Use RCTEventEmitter for event streams. Dispatch expensive work to background queues and keep the main thread for UI only.
4) TurboModules and Codegen for type safety
For new code, prefer TurboModules. Define the module specification in a .ts or .js interface that Codegen consumes to generate native scaffolding for iOS and Android. This gives you:
- Compile-time shape checking between JavaScript and native.
- Faster calls with less marshalling.
- Automatic autolinking metadata.
Keep the spec narrow and stable. Version capabilities with feature flags or optional methods rather than breaking changes.
5) JSI for synchronous and high-throughput work
When you must share memory, run tight loops, or perform synchronous operations (for example, cryptography, image processing, databases), build a JSI binding. Expose C++ functions and HostObjects that attach to the JavaScript runtime during an install() step. Avoid global state, guard against re-entrancy, and carefully manage lifetime across app reloads. Keep allocations on the native side and avoid copying large buffers across JavaScript.
6) Events and streaming
For push-style updates, use RCTEventEmitter (iOS) and DeviceEventEmitter (Android) or a TurboModule event pattern. Debounce noisy streams and provide start() and stop() methods so JavaScript controls subscriptions. Ensure addListener and removeListeners are implemented to prevent memory leaks.
7) Threading, performance, and idempotency
Never block the JavaScript thread. Offload I/O and CPU to background queues, and batch work to reduce chattiness. Make native operations idempotent where possible and guard against concurrent calls with mutexes or serial executors. If you provide synchronous JSI functions, document that they must be fast and non-blocking.
8) Errors, retries, and resilience
Map native failures to structured JavaScript errors with code, message, and details. Do not leak platform-specific exceptions. Implement timeouts for operations that cross process or network boundaries. Add safe retries with backoff only for transient cases. For irreversible actions, expose dry-run or capability APIs so JavaScript can preflight intent.
9) Packaging, autolinking, and configuration
Ship a complete package:
- A podspec for iOS, Gradle configuration for Android, and react-native.config.js for autolinking.
- Minimal setup steps, including permissions, Info.plist, AndroidManifest, ProGuard, and Provisional Entitlements.
- Optional build flags to toggle TurboModule or legacy bridge paths for backward compatibility.
10) Testing, CI, and example app
Add unit tests in native code and JavaScript. Provide an example app in the repository that exercises every method and event across cold start, fast refresh, background, and foreground. Run CI on macOS and Linux to build both platforms, lint the TypeScript spec, and verify Codegen artifacts are up to date. Add Detox or end-to-end smoke tests for critical flows.
11) Security, privacy, and permissions
Validate and sanitize inputs, never log secrets, and minimize data crossing the boundary. Gate sensitive features with explicit user consent. Fail safely when permissions are missing and provide a helper that opens system settings.
12) Migration and semantics
If you are replacing an existing JavaScript implementation, keep the public API compatible. Mark deprecated methods, provide feature detection (isAvailable, getConstants), and support both legacy bridge and TurboModule for one release cycle to ease adoption.
By starting with a typed JavaScript contract, selecting the correct integration path, and hardening the native implementation for performance, lifecycle, and security, you can integrate native modules or custom bridges in React Native that feel first-class, are testable, and scale with product needs.
Table
Common Mistakes
Blocking the JavaScript thread with synchronous work or long native calls. Copying large payloads over the legacy bridge instead of using JSI. Exporting a wide, unstable surface that mirrors platform quirks rather than a clean domain API. Forgetting lifecycle and leaking Activity or observers. Emitting events without addListener or removeListeners, causing memory growth. Returning platform exceptions directly instead of structured errors. Skipping permissions and failing at runtime without guidance. Shipping a module that cannot autolink, lacks an example app, or has no tests. Ignoring backward compatibility when migrating to TurboModules.
Sample Answers (Junior / Mid / Senior)
Junior:
“I define a small TypeScript API and implement a native module. On Android I export methods with @ReactMethod and return promises; on iOS I use RCT_EXPORT_METHOD. I move heavy work to background threads and send events through an emitter. I validate inputs and map native errors to { code, message }.”
Mid:
“For higher performance I use TurboModules with Codegen so the TypeScript spec generates native scaffolding. I keep the public API stable, add feature detection, and implement events with proper listener management. Packaging includes autolinking, permissions, and an example app, and CI builds both platforms.”
Senior:
“When calls are frequent or data is large I use JSI to expose synchronous C++ functions and HostObjects with an install() step. I design for lifecycle safety, avoid strong references to Activity, and ensure thread correctness. I version the API, provide a legacy bridge fallback, and ship tests, profiling, and security reviews.”
Evaluation Criteria
Look for a candidate who begins with a typed JavaScript contract, selects between classic modules, TurboModules, and JSI based on latency and data needs, and understands lifecycle, threading, and event patterns. Strong answers emphasize structured errors, background execution, input validation, permissions, and autolinking. They mention Codegen, HostObjects, and migration strategies with fallbacks. Red flags include blocking the JavaScript thread, dumping large blobs through the legacy bridge, leaky event emitters, untyped or unstable APIs, and missing packaging or tests.
Preparation Tips
Build a sample package that exposes a simple cryptography or image transform. Start with a TypeScript spec and generate a TurboModule with Codegen. Add an alternative JSI path for a synchronous transform and measure latency. Implement events and listener counts, ensure background execution, and verify no UI jank. Package with a podspec, Gradle files, and autolinking. Include an example app that toggles between legacy module, TurboModule, and JSI to compare behavior. Add permissions as needed, structured errors, and Detox tests. Run CI on macOS and Linux, and publish a pre-release to validate installation.
Real-world Context
A media app needed frame-accurate waveform rendering. The team replaced a chatty bridge module with a JSI binding and C++ core, dropping render time by more than half and eliminating jank. An enterprise app added hardware security key support via a TurboModule with Codegen, delivering type-safe APIs and painless autolinking. Another product shipped a sensor stream with proper addListener and removeListeners, fixing memory leaks. In each case, a narrow TypeScript surface, background execution, structured errors, and thorough packaging turned difficult native capabilities into reliable, reusable modules.
Key Takeaways
- Start with a narrow, typed JavaScript contract and validate on both sides.
- Choose classic modules for simple asynchronous calls, TurboModules for speed, and JSI for synchronous or high-throughput needs.
- Never block the JavaScript thread; manage events and lifecycle carefully.
- Ship structured errors, permissions, autolinking, tests, and an example app.
- Provide migration paths and fallbacks to maintain backward compatibility.
Practice Exercise
Scenario:
You must expose a device capability that React Native does not support natively: on-device image hashing used in a critical security flow. The hashing must be fast, available on both platforms, and callable during a sign-in screen without dropping frames.
Tasks:
- Define a minimal TypeScript API: hashImage(uri: string): Promise<string> plus isAvailable(): boolean. Include a synchronous variant guarded behind a feature flag for benchmarking.
- Implement a TurboModule with Codegen for the asynchronous version. On Android, write Kotlin code that loads a native C++ library; on iOS, expose a Swift wrapper. Offload work to background threads and validate input URIs and sizes.
- Implement a JSI binding that exposes a synchronous hashImageSync(buffer) using a C++ core and zero-copy views. Provide an install() initializer and ensure the function is small and non-blocking.
- Add structured error mapping with { code, message, details } for invalid input, memory limits, and permission issues.
- Package with autolinking, podspec, Gradle, and a README that documents permissions and fallback behavior.
- Create an example app that toggles between asynchronous TurboModule and synchronous JSI, logs timings, and verifies there is no frame loss.
- Add unit tests, instrumentation tests, and Detox flows; wire CI to build both platforms and lint Codegen outputs.
Deliverable:
A cross-platform module that demonstrates React Native native modules, TurboModules, and JSI bridges, with robust error handling, performance, packaging, and a clear migration and fallback story.

