Building Custom Native Modules for React Native

React Native|SEPTEMBER 9, 2025|0 VIEWS
A comprehensive guide to creating custom native modules that bridge JavaScript and native platform APIs

Building Custom Native Modules for React Native

React Native's strength lies in its ability to bridge JavaScript and native platform code, allowing developers to access platform-specific APIs and functionality. While React Native provides many built-in modules, there are times when you need to create custom native modules to:

  • Access platform-specific APIs not covered by React Native
  • Integrate third-party native libraries
  • Optimize performance-critical operations
  • Implement custom UI components with native behavior

In this comprehensive guide, we'll explore how to build custom native modules for both iOS and Android platforms.

Understanding Native Modules

Native modules are JavaScript classes that expose native platform functionality to your React Native application. They act as a bridge between your JavaScript code and the underlying native platform APIs.

Key Concepts

  • Bridge Communication: Native modules communicate with JavaScript through React Native's bridge
  • Asynchronous by Default: Most native module methods are asynchronous to avoid blocking the JavaScript thread
  • Platform-Specific Implementation: Each platform (iOS/Android) requires its own implementation

Setting Up the Development Environment

Before creating custom native modules, ensure you have the necessary development tools:

For iOS Development

# Install Xcode from the App Store
# Install CocoaPods
sudo gem install cocoapods

For Android Development

# Install Android Studio
# Set up Android SDK and build tools
# Configure environment variables (ANDROID_HOME, etc.)

Creating Your First Native Module

Let's create a simple native module that provides device information. We'll call it DeviceInfoModule.

Step 1: Create the JavaScript Interface

First, create the JavaScript interface that will be used by your React Native app:

// DeviceInfoModule.js
import { NativeModules } from "react-native";

const { DeviceInfoModule } = NativeModules;

export default {
  getDeviceId: () => {
    return DeviceInfoModule.getDeviceId();
  },

  getBatteryLevel: () => {
    return DeviceInfoModule.getBatteryLevel();
  },

  getSystemVersion: () => {
    return DeviceInfoModule.getSystemVersion();
  },
};

Step 2: Implement the iOS Native Module

Create the iOS implementation using Objective-C or Swift:

Objective-C Implementation

Create DeviceInfoModule.h:

// DeviceInfoModule.h
#import <React/RCTBridgeModule.h>

@interface DeviceInfoModule : NSObject <RCTBridgeModule>
@end

Create DeviceInfoModule.m:

// DeviceInfoModule.m
#import "DeviceInfoModule.h"
#import <React/RCTLog.h>
#import <UIKit/UIKit.h>

@implementation DeviceInfoModule

// Export module name to JavaScript
RCT_EXPORT_MODULE();

// Export method to get device ID
RCT_EXPORT_METHOD(getDeviceId:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  NSString *deviceId = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
  if (deviceId) {
    resolve(deviceId);
  } else {
    reject(@"device_id_error", @"Could not retrieve device ID", nil);
  }
}

// Export method to get battery level
RCT_EXPORT_METHOD(getBatteryLevel:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  [UIDevice currentDevice].batteryMonitoringEnabled = YES;
  float batteryLevel = [UIDevice currentDevice].batteryLevel;

  if (batteryLevel >= 0) {
    resolve(@(batteryLevel * 100)); // Convert to percentage
  } else {
    reject(@"battery_error", @"Could not retrieve battery level", nil);
  }
}

// Export method to get system version
RCT_EXPORT_METHOD(getSystemVersion:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  NSString *systemVersion = [[UIDevice currentDevice] systemVersion];
  resolve(systemVersion);
}

@end

Swift Implementation (Alternative)

If you prefer Swift, create a bridging header and implement in Swift:

// DeviceInfoModule.swift
import Foundation
import UIKit

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {

  @objc
  func getDeviceId(_ resolve: @escaping RCTPromiseResolveBlock,
                   rejecter reject: @escaping RCTPromiseRejectBlock) {
    if let deviceId = UIDevice.current.identifierForVendor?.uuidString {
      resolve(deviceId)
    } else {
      reject("device_id_error", "Could not retrieve device ID", nil)
    }
  }

  @objc
  func getBatteryLevel(_ resolve: @escaping RCTPromiseResolveBlock,
                       rejecter reject: @escaping RCTPromiseRejectBlock) {
    UIDevice.current.isBatteryMonitoringEnabled = true
    let batteryLevel = UIDevice.current.batteryLevel

    if batteryLevel >= 0 {
      resolve(batteryLevel * 100) // Convert to percentage
    } else {
      reject("battery_error", "Could not retrieve battery level", nil)
    }
  }

  @objc
  func getSystemVersion(_ resolve: @escaping RCTPromiseResolveBlock,
                        rejecter reject: @escaping RCTPromiseRejectBlock) {
    resolve(UIDevice.current.systemVersion)
  }

  @objc
  static func requiresMainQueueSetup() -> Bool {
    return false
  }
}

Don't forget the bridging header (DeviceInfoModule-Bridging-Header.h):

#import <React/RCTBridgeModule.h>

Step 3: Implement the Android Native Module

Create the Android implementation using Java or Kotlin:

Java Implementation

Create DeviceInfoModule.java:

// DeviceInfoModule.java
package com.yourapp.modules;

import android.content.Context;
import android.content.IntentFilter;
import android.content.Intent;
import android.os.BatteryManager;
import android.os.Build;
import android.provider.Settings;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;

public class DeviceInfoModule extends ReactContextBaseJavaModule {

    private static ReactApplicationContext reactContext;

    DeviceInfoModule(ReactApplicationContext context) {
        super(context);
        reactContext = context;
    }

    @Override
    public String getName() {
        return "DeviceInfoModule";
    }

    @ReactMethod
    public void getDeviceId(Promise promise) {
        try {
            String deviceId = Settings.Secure.getString(
                reactContext.getContentResolver(),
                Settings.Secure.ANDROID_ID
            );
            promise.resolve(deviceId);
        } catch (Exception e) {
            promise.reject("device_id_error", "Could not retrieve device ID", e);
        }
    }

    @ReactMethod
    public void getBatteryLevel(Promise promise) {
        try {
            IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
            Intent batteryStatus = reactContext.registerReceiver(null, ifilter);

            int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
            int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);

            float batteryPct = level * 100 / (float) scale;
            promise.resolve(batteryPct);
        } catch (Exception e) {
            promise.reject("battery_error", "Could not retrieve battery level", e);
        }
    }

    @ReactMethod
    public void getSystemVersion(Promise promise) {
        try {
            promise.resolve(Build.VERSION.RELEASE);
        } catch (Exception e) {
            promise.reject("system_version_error", "Could not retrieve system version", e);
        }
    }
}

Create the package class DeviceInfoPackage.java:

// DeviceInfoPackage.java
package com.yourapp.modules;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class DeviceInfoPackage implements ReactPackage {

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new DeviceInfoModule(reactContext));
        return modules;
    }
}

Kotlin Implementation (Alternative)

// DeviceInfoModule.kt
package com.yourapp.modules

import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import android.provider.Settings
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise

class DeviceInfoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

    override fun getName(): String {
        return "DeviceInfoModule"
    }

    @ReactMethod
    fun getDeviceId(promise: Promise) {
        try {
            val deviceId = Settings.Secure.getString(
                reactApplicationContext.contentResolver,
                Settings.Secure.ANDROID_ID
            )
            promise.resolve(deviceId)
        } catch (e: Exception) {
            promise.reject("device_id_error", "Could not retrieve device ID", e)
        }
    }

    @ReactMethod
    fun getBatteryLevel(promise: Promise) {
        try {
            val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
            val batteryStatus = reactApplicationContext.registerReceiver(null, intentFilter)

            val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
            val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1

            val batteryPercent = level * 100 / scale.toFloat()
            promise.resolve(batteryPercent)
        } catch (e: Exception) {
            promise.reject("battery_error", "Could not retrieve battery level", e)
        }
    }

    @ReactMethod
    fun getSystemVersion(promise: Promise) {
        try {
            promise.resolve(Build.VERSION.RELEASE)
        } catch (e: Exception) {
            promise.reject("system_version_error", "Could not retrieve system version", e)
        }
    }
}

Step 4: Register the Module

For Android

In your MainApplication.java, add the package:

// MainApplication.java
import com.yourapp.modules.DeviceInfoPackage;

@Override
protected List<ReactPackage> getPackages() {
    @SuppressWarnings("UnnecessaryLocalVariable")
    List<ReactPackage> packages = new PackageList(this).getPackages();
    packages.add(new DeviceInfoPackage()); // Add this line
    return packages;
}

For iOS

No additional registration is needed for iOS. The module will be automatically discovered.

Step 5: Use the Module in Your React Native App

// App.js
import React, { useEffect, useState } from "react";
import { View, Text, Button, Alert } from "react-native";
import DeviceInfoModule from "./DeviceInfoModule";

const App = () => {
  const [deviceInfo, setDeviceInfo] = useState({});

  const getDeviceInfo = async () => {
    try {
      const [deviceId, batteryLevel, systemVersion] = await Promise.all([
        DeviceInfoModule.getDeviceId(),
        DeviceInfoModule.getBatteryLevel(),
        DeviceInfoModule.getSystemVersion(),
      ]);

      setDeviceInfo({
        deviceId,
        batteryLevel: `${batteryLevel.toFixed(1)}%`,
        systemVersion,
      });
    } catch (error) {
      Alert.alert("Error", error.message);
    }
  };

  return (
    <View style={{ flex: 1, justifyContent: "center", padding: 20 }}>
      <Button title="Get Device Info" onPress={getDeviceInfo} />

      {Object.keys(deviceInfo).length > 0 && (
        <View style={{ marginTop: 20 }}>
          <Text>Device ID: {deviceInfo.deviceId}</Text>
          <Text>Battery Level: {deviceInfo.batteryLevel}</Text>
          <Text>System Version: {deviceInfo.systemVersion}</Text>
        </View>
      )}
    </View>
  );
};

export default App;

Advanced Features

Event Emitters

For continuous data streams or events, you can use event emitters:

iOS Implementation

// DeviceInfoModule.m
#import <React/RCTEventEmitter.h>

@interface DeviceInfoModule : RCTEventEmitter <RCTBridgeModule>
@end

@implementation DeviceInfoModule

RCT_EXPORT_MODULE();

- (NSArray<NSString *> *)supportedEvents {
    return @[@"BatteryLevelChanged"];
}

- (void)startObserving {
    [[NSNotificationCenter defaultCenter]
        addObserver:self
        selector:@selector(batteryLevelChanged:)
        name:UIDeviceBatteryLevelDidChangeNotification
        object:nil];

    [UIDevice currentDevice].batteryMonitoringEnabled = YES;
}

- (void)stopObserving {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)batteryLevelChanged:(NSNotification *)notification {
    float batteryLevel = [UIDevice currentDevice].batteryLevel;
    [self sendEventWithName:@"BatteryLevelChanged"
                       body:@{@"level": @(batteryLevel * 100)}];
}

@end

Android Implementation

// DeviceInfoModule.java
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;

public class DeviceInfoModule extends ReactContextBaseJavaModule {

    private void sendEvent(ReactApplicationContext reactContext,
                          String eventName,
                          WritableMap params) {
        reactContext
            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit(eventName, params);
    }

    @ReactMethod
    public void startBatteryMonitoring() {
        // Register broadcast receiver for battery changes
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_BATTERY_CHANGED);

        BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                float batteryPct = level * 100 / (float) scale;

                WritableMap params = Arguments.createMap();
                params.putDouble("level", batteryPct);
                sendEvent(reactContext, "BatteryLevelChanged", params);
            }
        };

        reactContext.registerReceiver(batteryReceiver, filter);
    }
}

Using Events in JavaScript

// App.js
import { DeviceEventEmitter } from "react-native";

useEffect(() => {
  const subscription = DeviceEventEmitter.addListener(
    "BatteryLevelChanged",
    (data) => {
      console.log("Battery level:", data.level);
    }
  );

  return () => subscription.remove();
}, []);

Synchronous Methods

For simple operations that don't require async processing:

iOS

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getDeviceName)
{
    return [[UIDevice currentDevice] name];
}

Android

@ReactMethod(isBlockingSynchronousMethod = true)
public String getDeviceName() {
    return Build.MODEL;
}

Best Practices

1. Error Handling

Always implement proper error handling with descriptive error messages:

// Good error handling
@ReactMethod
public void riskyOperation(Promise promise) {
    try {
        // Perform operation
        String result = performOperation();
        promise.resolve(result);
    } catch (SecurityException e) {
        promise.reject("SECURITY_ERROR", "Permission denied", e);
    } catch (Exception e) {
        promise.reject("UNKNOWN_ERROR", "An unexpected error occurred", e);
    }
}

2. Threading Considerations

iOS

// Run on main thread for UI operations
RCT_EXPORT_METHOD(updateUI:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(dispatch_get_main_queue(), ^{
        // UI operations here
        resolve(@"Updated");
    });
}

// Run on background thread for heavy operations
RCT_EXPORT_METHOD(heavyOperation:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // Heavy computation here
        dispatch_async(dispatch_get_main_queue(), ^{
            resolve(@"Completed");
        });
    });
}

Android

@ReactMethod
public void heavyOperation(Promise promise) {
    AsyncTask.execute(() -> {
        try {
            // Heavy computation on background thread
            String result = performHeavyComputation();

            // Resolve on main thread
            new Handler(Looper.getMainLooper()).post(() -> {
                promise.resolve(result);
            });
        } catch (Exception e) {
            promise.reject("COMPUTATION_ERROR", e.getMessage(), e);
        }
    });
}

3. Memory Management

iOS

  • Use weak references to avoid retain cycles
  • Clean up observers and timers in dealloc

Android

  • Remove listeners and callbacks
  • Use weak references where appropriate
  • Clean up in module lifecycle methods

4. Testing Native Modules

Create unit tests for your native modules:

iOS Testing

// DeviceInfoModuleTests.m
#import <XCTest/XCTest.h>
#import "DeviceInfoModule.h"

@interface DeviceInfoModuleTests : XCTestCase
@property (nonatomic, strong) DeviceInfoModule *module;
@end

@implementation DeviceInfoModuleTests

- (void)setUp {
    self.module = [[DeviceInfoModule alloc] init];
}

- (void)testGetSystemVersion {
    XCTestExpectation *expectation = [self expectationWithDescription:@"Get system version"];

    [self.module getSystemVersion:^(id result) {
        XCTAssertNotNil(result);
        XCTAssertTrue([result isKindOfClass:[NSString class]]);
        [expectation fulfill];
    } rejecter:^(NSString *code, NSString *message, NSError *error) {
        XCTFail(@"Should not reject");
    }];

    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}

@end

Android Testing

// DeviceInfoModuleTest.java
@RunWith(RobolectricTestRunner.class)
public class DeviceInfoModuleTest {

    private DeviceInfoModule module;

    @Before
    public void setUp() {
        ReactApplicationContext context = mock(ReactApplicationContext.class);
        module = new DeviceInfoModule(context);
    }

    @Test
    public void testGetSystemVersion() {
        Promise promise = mock(Promise.class);
        module.getSystemVersion(promise);

        verify(promise).resolve(anyString());
    }
}

Common Pitfalls and Solutions

1. Bridge Communication Overhead

  • Minimize data passing between native and JavaScript
  • Batch operations when possible
  • Use native UI components for performance-critical interfaces

2. Platform Differences

  • Abstract platform differences in your JavaScript interface
  • Use feature detection rather than platform detection
  • Provide graceful fallbacks for unsupported features

3. Permissions

  • Always check and request permissions before accessing sensitive APIs
  • Provide clear error messages for permission denials
  • Handle permission state changes gracefully
// Example: Handling permissions
const requestLocationPermission = async () => {
  try {
    if (Platform.OS === "android") {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
      );
      return granted === PermissionsAndroid.RESULTS.GRANTED;
    }
    return true; // iOS handles permissions differently
  } catch (error) {
    console.warn("Permission request failed:", error);
    return false;
  }
};

Conclusion

Building custom native modules for React Native opens up endless possibilities for integrating platform-specific functionality into your applications. By following the patterns and best practices outlined in this guide, you can create robust, performant native modules that seamlessly bridge the gap between JavaScript and native platform APIs.

Remember to:

  • Start with a clear JavaScript interface
  • Implement platform-specific code following native conventions
  • Handle errors gracefully
  • Consider threading and performance implications
  • Test thoroughly on both platforms
  • Document your modules well for future maintenance

With these foundations, you'll be well-equipped to extend React Native's capabilities and create truly native experiences for your users.

Further Reading