React Native does a pretty good job handling most screens and business logic with just JavaScript. For a lot of apps, that’s all you need. But sooner or later, you’ll run into some walls. If your app needs to get close to the hardware stuff like NFC, Bluetooth, sensors, or the camera JavaScript alone doesn’t cut it. The same problem pops up if you want to run background tasks, use widgets or app extensions, or connect to native SDKs. JS just isn’t built for these deep hooks into the device.

Performance can be another headache. Heavy work like video or audio processing, machine learning, or encryption tends to drag if you keep everything in JavaScript.

At some point, you end up fighting the framework instead of building your app. That’s where bridging comes in. By creating Native Modules or using the newer JSI and Turbo Modules in React Native’s latest architecture you hook your app directly into native iOS or Android APIs. Suddenly, all those features JavaScript can’t reach are right there, and performance gets a real boost.

Why use Bridging Native Modules?

Access to native APIs
Some device features are not available in React Native or through existing libraries. If your app requires advanced sensors, Bluetooth operations, HealthKit data, custom camera functionality, or system-level services, you will need native code.

Handles heavy tasks more smoothly
Implementation features like cryptography, image or video processing, complex calculations, and animations run much faster and more reliably in native code than in JS.

Integrating third-party native SDKs
Many popular SDKs, such as authentication, payments, analytics, ads, and media tools, are only available for Android and iOS. Bridging is necessary to link these SDKs with your React Native app.

Platform-specific behaviour or UI
Sometimes an app needs custom components or behaviours that vary significantly between Android and iOS. In these situations, using native modules or native UI components gives you full control over each platform’s experience.

Architecture Evolution: The Legacy Bridge vs. JSI

To understand bridging, you must understand how React Native communicates with the native side.

The Legacy Bridge (Pre-0.68)

When your app calls a native module, it all starts on the JavaScript side where your React code is doing its thing. Before it can conversion with the native side, the data gets packed up into a format that can safely cross over.

It then goes through the Bridge, which works like a messenger between JavaScript and native. Once it reaches native, the data is unpacked and converted into platform-specific types, and the method runs on the native thread (moving to the main thread if it needs to update the UI).

When the work is done, the response follows the same path back to JavaScript.

JS Thread → JSON Serialization → [Bridge] → JSON Deserialization → Native Thread

How native modules work in react native

The New Architecture: JSI (JavaScript Interface)

With JSI, JavaScript no longer needs to serialize data to communicate with the native side. It can directly reference C++ objects and call their methods instantly — synchronously, without the back-and-forth lag that made the old bridge feel slow. The outcome is noticeably quicker and more efficient native interactions.

JS Thread ↔ JSI Layer ↔ [C++ Host Objects] ↔ Native Thread

How the new react native jsi-based architecture works

Native Modules vs. Native UI Components

Use Native Modules when…

You need native logic or system functionality that does not directly render UI.

Typical scenarios

  • Accessing device APIs
    • Camera permissions
    • BT, NFC (Near-field communication)
    • Health data of HealthKit or Google Fit
    • File system, sensors
  • Running native computations
  • Triggering native services
  • Background tasks
  • Integration of third-party native SDKs (analytics, payments, ads)

Use Native UI Components when…

You need to display a native view on the screen

Typical scenarios

  • Custom views not available in React Native
  • Platform-specific UI
    • Native maps
    • Custom Push Notification
    • Video players
    • Camera preview
    • Native UI elements like charts, calendars, and pickers
    • Reusing an existing native UI library inside React Native

Implementing Your First Bridge

Let’s put together a basic Device Information module that gives you the device name. We’ll handle both iOS (using Swift) and Android (with Kotlin). There’s no user interface here, just native features under the hood, so we’ll set it up as a Native Module.

Step 1: Set up the JS Interface

First, create a file called DeviceInfo.js to house our JavaScript module. This is what your React components will import.

Set up the JS Interface

Step 2: In Android (Kotlin)

Go to your Android project folder at android/app/src/main/java/com/yourapp/ and make a new file called DeviceInfoModule.kt.

In Android (Kotlin)

Then, Let’s register the module with React Native by creating a package.

Create DeviceInfoPackage.kt:

DeviceInfoPackage.kt

Important: Add this package to your MainApplication.java’s getPackages list:

Step 3: In iOS (Swift)

Create a new Swift file in your Xcode project DeviceInfo.swift

In iOS (Swift)

To expose the module to React Native, create an Objective-C file DeviceInfo.m

Step 4: Working with the React Native Bridge

At this point, you can start using your new native module from any React component.

Working with the React Native Bridge

Handling Data Types, Callbacks, and Threading

Data Types

The bridge automatically converts JavaScript types to native types:

JavaScript Type Android (Java/Kotlin) iOS (Objective-C/Swift)
String String NSString/String
Boolean Boolean NSNumber/Bool
Number Integer, Double, Float NSNumber/Number
Array ReadableArray NSArray/[Any]
Object ReadableMap NSDictionary/[String: Any]
null null NSNull/Null

 

Callbacks vs. Promises

Using Callbacks (Android Kotlin):

Using Callbacks (Android Kotlin):

Using Callbacks (iOS Swift):

Using Callbacks (iOS Swift):

Threading Considerations

Thread Purpose When to Use
Background Thread Heavy computations, network calls, file I/O Default for most native module methods
Main/UI Thread UI updates, rendering For Native UI Components or when updating views

 

Android – Ensuring Main Thread Execution:

Android - Ensuring Main Thread Execution

iOS – Ensuring Main Thread Execution:

If you need UI work:

iOS - Ensuring Main Thread Execution:

Performance Considerations & Common Pitfalls

Performance Best Practices

Practice Why How
Avoid Large Data Over Bridge Serialization overhead causes bottlenecks Pass URIs instead of raw data
Batch Operations Reduce bridge crossings Combine multiple calls into one
Use Background Threads Prevent UI blocking Offload heavy work to separate threads
Cache Results Avoid repeated native calls Implement caching on native side

Common Pitfalls

  1. Memory Leaks in Android:

Memory Leaks in Android

     2. Retain Cycles in iOS:

Retain Cycles in iOS:

     3. Synchronous Methods Blocking JS Thread:

AVOID this type of syntax

const result = NativeModules.MyModule.synchronousHeavyTask();

PREFER – Async is better approach

const result = await NativeModules.MyModule.asyncHeavyTask();

Best Practices for Cross-Platform Consistency

When building native modules for both iOS and Android, consistency is critical especially during module initialization and loading.

Best Practice:

  • Avoid heavy logic in constructors / init blocks
  • Do not perform network calls or disk reads during module load
  • Initialize resources lazily (only when a method is called)

Good Practice

MyModule.initialize(config);

MyModule.scanDevices(timeout);

Both platforms must implement the same method names and parameter behavior.

Avoid:

if (Platform.OS === ‘ios’) {
MyModule.startScan();
} else {
MyModule.scanDevices();
}

Real-World Use Cases

Case Study 1: Custom Camera Module

// Android Camera Module

@ReactMethod

fun startCamera(options: ReadableMap, promise: Promise) {

val cameraId = options.getString(“cameraId”)

val resolution = options.getString(“resolution”)

 

// Initialize native camera with custom parameters

val camera = CameraController.getInstance()

camera.initialize(cameraId, resolution)

 

// Set up frame processor for real-time analysis

camera.setFrameProcessor { frame ->

// Process frame for QR code, face detection, etc.

val result = processFrame(frame)

if (result != null) {

sendEvent(“onFrameProcessed”, result)

}

}

promise.resolve(“Camera started successfully”)

}

Case Study 2: Bluetooth Low Energy Integration

// iOS BLE Module

@objc func connectToDevice(_ deviceId: String,

resolver resolve: @escaping RCTPromiseResolveBlock,

rejecter reject: @escaping RCTPromiseRejectBlock) {

let centralManager = CBCentralManager(delegate: self, queue: nil)

// Store resolve/reject for later use

connectionPromise = (resolve, reject)

// Start connection process

guard let uuid = UUID(uuidString: deviceId) else {

reject(“INVALID_UUID”, “Invalid device identifier”, nil)

return

}

 

let device = centralManager.retrievePeripherals(withIdentifiers: [uuid]).first

centralManager.connect(device, options: nil)

}

Case Study 3: Sensor Data Streaming

// React Component for sensor data

import React, { useEffect, useState } from ‘react’;

import { NativeModules, NativeEventEmitter } from ‘react-native’;

 

const { SensorModule } = NativeModules;

const sensorEmitter = new NativeEventEmitter(SensorModule);

 

export const SensorMonitor = () => {

const [accelerometerData, setAccelerometerData] = useState({ x: 0, y: 0, z: 0 });

 

useEffect(() => {

// Start listening to sensor events

const subscription = sensorEmitter.addListener(‘onSensorData’, (data) => {

if (data.type === ‘accelerometer’) {

setAccelerometerData({

x: data.values[0],

y: data.values[1],

z: data.values[2],

});

}

});

 

// Start sensor streaming

SensorModule.startSensorStreaming(‘accelerometer’, 100); // 100ms interval

 

return () => {

subscription.remove();

SensorModule.stopSensorStreaming(‘accelerometer’);

};

}, []);

 

return (

<View>

<Text>Accelerometer: X={accelerometerData.x}, Y={accelerometerData.y}, Z={accelerometerData.z}</Text>

</View>

);

};

Conclusion

Bridging native code in React Native is a superpower that removes the “glass ceiling” of the framework, allowing you to build anything a fully native app can build. By understanding the architecture, respecting the threading model, and following the best practices outlined above, you can create seamless, high-performance bridges that feel right at home in the React Native ecosystem.

Unlock the Full Potential of React Native

From custom native modules to advanced device integrations, DEV IT helps you build faster, smarter, and more powerful React Native applications that scale with your business.

Schedule a Consultation