home / skills / thebushidocollective / han / react-native-native-modules

This skill helps you build and integrate native modules for React Native, enabling Turbo Modules, bridging, and platform-specific APIs.

npx playbooks add skill thebushidocollective/han --skill react-native-native-modules

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
12.8 KB
---
name: react-native-native-modules
user-invocable: false
description: Use when building or integrating native modules in React Native. Covers creating native modules, Turbo Modules, bridging native code, and accessing platform-specific APIs.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Grep
  - Glob
---

# React Native Native Modules

Use this skill when creating custom native modules, integrating third-party native libraries, or accessing platform-specific functionality not available through JavaScript.

## Key Concepts

### Native Modules Overview

Native modules bridge JavaScript and native code:

```
JavaScript Layer
      ↕ (Bridge)
Native Layer (iOS/Android)
```

### Turbo Modules (Modern Approach)

Turbo Modules provide better performance with type safety:

```tsx
// NativeMyModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getString(value: string): Promise<string>;
  getNumber(value: number): number;
  getBoolean(value: boolean): boolean;
  getArray(value: Array<any>): Array<any>;
  getObject(value: Object): Object;
}

export default TurboModuleRegistry.getEnforcing<Spec>('MyModule');
```

### Calling Native Code from JS

```tsx
import { NativeModules } from 'react-native';

const { MyModule } = NativeModules;

// Call native method
async function callNativeMethod() {
  try {
    const result = await MyModule.getString('Hello from JS');
    console.log(result);
  } catch (error) {
    console.error('Native module error:', error);
  }
}
```

## Best Practices

### iOS Native Module (Swift)

Create a native module in Swift:

```swift
// MyModule.swift
import Foundation

@objc(MyModule)
class MyModule: NSObject {

  @objc
  func getString(_ value: String,
                 resolver: @escaping RCTPromiseResolveBlock,
                 rejecter: @escaping RCTPromiseRejectBlock) {
    // Process value
    let result = "Processed: \(value)"
    resolver(result)
  }

  @objc
  func getNumber(_ value: NSNumber) -> NSNumber {
    let doubled = value.doubleValue * 2
    return NSNumber(value: doubled)
  }

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

```objc
// MyModule.m (Bridge file)
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(MyModule, NSObject)

RCT_EXTERN_METHOD(getString:(NSString *)value
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getNumber:(nonnull NSNumber *)value)

@end
```

### Android Native Module (Kotlin)

Create a native module in Kotlin:

```kotlin
// MyModule.kt
package com.myapp

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 MyModule(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext) {

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

    @ReactMethod
    fun getString(value: String, promise: Promise) {
        try {
            val result = "Processed: $value"
            promise.resolve(result)
        } catch (e: Exception) {
            promise.reject("ERROR", e.message)
        }
    }

    @ReactMethod
    fun getNumber(value: Double): Double {
        return value * 2
    }
}
```

```kotlin
// MyModulePackage.kt
package com.myapp

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

class MyModulePackage : ReactPackage {
    override fun createNativeModules(
        reactContext: ReactApplicationContext
    ): List<NativeModule> {
        return listOf(MyModule(reactContext))
    }

    override fun createViewManagers(
        reactContext: ReactApplicationContext
    ): List<ViewManager<*, *>> {
        return emptyList()
    }
}
```

### TypeScript Wrapper

Create a type-safe wrapper:

```tsx
// MyModule.ts
import { NativeModules, Platform } from 'react-native';

interface MyModuleInterface {
  getString(value: string): Promise<string>;
  getNumber(value: number): number;
  getBoolean(value: boolean): boolean;
}

const LINKING_ERROR =
  `The package 'react-native-my-module' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({ ios: "- Run 'pod install'\n", default: '' }) +
  '- Rebuild the app';

const MyModule: MyModuleInterface = NativeModules.MyModule
  ? NativeModules.MyModule
  : new Proxy(
      {},
      {
        get() {
          throw new Error(LINKING_ERROR);
        },
      }
    );

export default MyModule;
```

### Native Events

Send events from native to JavaScript:

```swift
// iOS - MyModule.swift
import Foundation

@objc(MyModule)
class MyModule: RCTEventEmitter {

  override func supportedEvents() -> [String]! {
    return ["onDataReceived"]
  }

  @objc
  func startListening() {
    // Simulate receiving data
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
      self.sendEvent(withName: "onDataReceived",
                     body: ["data": "Hello from native!"])
    }
  }
}
```

```kotlin
// Android - MyModule.kt
class MyModule(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext) {

    private fun sendEvent(eventName: String, params: WritableMap?) {
        reactApplicationContext
            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
            .emit(eventName, params)
    }

    @ReactMethod
    fun startListening() {
        val params = Arguments.createMap()
        params.putString("data", "Hello from native!")
        sendEvent("onDataReceived", params)
    }
}
```

```tsx
// JavaScript
import { NativeEventEmitter, NativeModules } from 'react-native';
import { useEffect } from 'react';

function useNativeEvent() {
  useEffect(() => {
    const eventEmitter = new NativeEventEmitter(NativeModules.MyModule);

    const subscription = eventEmitter.addListener('onDataReceived', (event) => {
      console.log('Received from native:', event.data);
    });

    NativeModules.MyModule.startListening();

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

## Common Patterns

### Camera Access

```tsx
// JavaScript API
interface CameraModule {
  takePicture(): Promise<string>; // Returns image URI
  requestPermissions(): Promise<boolean>;
}
```

```swift
// iOS Implementation
import UIKit
import AVFoundation

@objc(CameraModule)
class CameraModule: NSObject {

  @objc
  func requestPermissions(_ resolve: @escaping RCTPromiseResolveBlock,
                         rejecter reject: @escaping RCTPromiseRejectBlock) {
    AVCaptureDevice.requestAccess(for: .video) { granted in
      resolve(granted)
    }
  }

  @objc
  func takePicture(_ resolve: @escaping RCTPromiseResolveBlock,
                   rejecter reject: @escaping RCTPromiseRejectBlock) {
    // Implement camera capture
    // Return image URI
    resolve("file:///path/to/image.jpg")
  }
}
```

```kotlin
// Android Implementation
class CameraModule(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext) {

    @ReactMethod
    fun requestPermissions(promise: Promise) {
        // Check and request camera permission
        val hasPermission = ContextCompat.checkSelfPermission(
            reactApplicationContext,
            Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED

        promise.resolve(hasPermission)
    }

    @ReactMethod
    fun takePicture(promise: Promise) {
        // Implement camera capture
        promise.resolve("file:///path/to/image.jpg")
    }
}
```

### Biometric Authentication

```tsx
// JavaScript API
interface BiometricModule {
  authenticate(reason: string): Promise<{ success: boolean; error?: string }>;
  isAvailable(): Promise<boolean>;
}
```

```swift
// iOS Implementation
import LocalAuthentication

@objc(BiometricModule)
class BiometricModule: NSObject {

  @objc
  func isAvailable(_ resolve: @escaping RCTPromiseResolveBlock,
                   rejecter reject: @escaping RCTPromiseRejectBlock) {
    let context = LAContext()
    var error: NSError?

    let available = context.canEvaluatePolicy(
      .deviceOwnerAuthenticationWithBiometrics,
      error: &error
    )

    resolve(available)
  }

  @objc
  func authenticate(_ reason: String,
                   resolver resolve: @escaping RCTPromiseResolveBlock,
                   rejecter reject: @escaping RCTPromiseRejectBlock) {
    let context = LAContext()

    context.evaluatePolicy(
      .deviceOwnerAuthenticationWithBiometrics,
      localizedReason: reason
    ) { success, error in
      if success {
        resolve(["success": true])
      } else {
        resolve(["success": false, "error": error?.localizedDescription ?? ""])
      }
    }
  }
}
```

### Device Info Module

```tsx
// JavaScript API
interface DeviceInfoModule {
  getDeviceId(): string;
  getDeviceName(): string;
  getSystemVersion(): string;
  getBatteryLevel(): Promise<number>;
}
```

```swift
// iOS Implementation
import UIKit

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {

  @objc
  func getDeviceId() -> String {
    return UIDevice.current.identifierForVendor?.uuidString ?? ""
  }

  @objc
  func getDeviceName() -> String {
    return UIDevice.current.name
  }

  @objc
  func getSystemVersion() -> String {
    return UIDevice.current.systemVersion
  }

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

### Native UI Component

```tsx
// Custom native view
import { requireNativeComponent, ViewProps } from 'react-native';

interface MapViewProps extends ViewProps {
  region: {
    latitude: number;
    longitude: number;
    latitudeDelta: number;
    longitudeDelta: number;
  };
  onRegionChange?: (event: any) => void;
}

export const MapView = requireNativeComponent<MapViewProps>('MapView');

// Usage
<MapView
  region={{
    latitude: 37.78825,
    longitude: -122.4324,
    latitudeDelta: 0.0922,
    longitudeDelta: 0.0421,
  }}
  onRegionChange={(event) => console.log(event.nativeEvent)}
/>
```

## Anti-Patterns

### Don't Block the Main Thread

```swift
// Bad - Blocking main thread
@objc
func heavyComputation(_ value: String,
                     resolver resolve: @escaping RCTPromiseResolveBlock,
                     rejecter reject: @escaping RCTPromiseRejectBlock) {
  let result = performHeavyWork(value) // Blocks UI
  resolve(result)
}

// Good - Use background thread
@objc
func heavyComputation(_ value: String,
                     resolver resolve: @escaping RCTPromiseResolveBlock,
                     rejecter reject: @escaping RCTPromiseRejectBlock) {
  DispatchQueue.global(qos: .userInitiated).async {
    let result = self.performHeavyWork(value)
    resolve(result)
  }
}
```

### Don't Forget Error Handling

```kotlin
// Bad - No error handling
@ReactMethod
fun readFile(path: String, promise: Promise) {
    val content = File(path).readText()
    promise.resolve(content)
}

// Good - Proper error handling
@ReactMethod
fun readFile(path: String, promise: Promise) {
    try {
        val file = File(path)
        if (!file.exists()) {
            promise.reject("FILE_NOT_FOUND", "File does not exist")
            return
        }
        val content = file.readText()
        promise.resolve(content)
    } catch (e: Exception) {
        promise.reject("READ_ERROR", e.message, e)
    }
}
```

### Don't Leak Memory

```swift
// Bad - Strong reference cycle
class MyModule: NSObject {
    var timer: Timer?

    @objc
    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.doSomething() // Strong reference to self
        }
    }
}

// Good - Weak reference
class MyModule: NSObject {
    var timer: Timer?

    @objc
    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.doSomething()
        }
    }

    deinit {
        timer?.invalidate()
    }
}
```

### Don't Use Synchronous Operations

```kotlin
// Bad - Synchronous network call
@ReactMethod
fun fetchData(url: String): String {
    return URL(url).readText() // Blocks thread
}

// Good - Asynchronous with promise
@ReactMethod
fun fetchData(url: String, promise: Promise) {
    Thread {
        try {
            val data = URL(url).readText()
            promise.resolve(data)
        } catch (e: Exception) {
            promise.reject("FETCH_ERROR", e.message)
        }
    }.start()
}
```

## Related Skills

- **react-native-platform**: Platform-specific code patterns
- **react-native-components**: Integrating native components
- **react-native-performance**: Native performance optimization

Overview

This skill helps developers create and integrate native modules for React Native on iOS and Android. It covers classic bridges, Turbo Modules, event emission, and TypeScript wrappers to provide type-safe, performant access to platform-specific APIs. Use it to expose native features, build custom UI components, and safely bridge long-running work to JavaScript.

How this skill works

The skill explains how JavaScript talks to native code via the React Native bridge and modern Turbo Modules for lower latency and stronger typing. It shows concrete implementations in Swift, Objective-C bridge files, and Kotlin plus a TypeScript wrapper to surface a safe JS API. It also covers sending native events to JS and registering native UI components.

When to use it

  • Access hardware or platform APIs not available in JS (camera, biometrics, sensors).
  • Improve performance using Turbo Modules for frequent native calls.
  • Integrate third-party native SDKs or custom platform libraries.
  • Expose custom native UI components to React Native.
  • Send asynchronous native events to JavaScript listeners.
  • Wrap native code with TypeScript for safer app-level usage.

Best practices

  • Avoid blocking the main thread; run heavy work on background queues or threads.
  • Use promises or callbacks for asynchronous results; never block JS synchronously.
  • Provide clear TypeScript interfaces and a linking error proxy for developer experience.
  • Handle errors explicitly and return meaningful error codes/messages to JS.
  • Prevent memory leaks: use weak references and invalidate timers on deinit.
  • Prefer Turbo Modules for high-frequency calls and strict typing when possible.

Example use cases

  • Camera module exposing takePicture and requestPermissions implementations for iOS and Android.
  • Biometric auth module that checks availability and performs secure authentication flows.
  • Device info module providing deviceId, deviceName, systemVersion, and battery level.
  • Custom native MapView component with events for region changes sent to JS.
  • Background service that streams sensor data to JavaScript via native events.

FAQ

When should I choose Turbo Modules over the classic bridge?

Use Turbo Modules for high-frequency or performance-sensitive calls and when you want compile-time type safety. Use the classic bridge for quick prototypes or when Turbo Module support is not available in your environment.

How do I avoid crashing the app when a native module isn’t linked?

Provide a TypeScript wrapper that throws a clear linking error and instructs developers to run platform-specific steps (pod install, rebuild). Use a Proxy fallback to surface the message at runtime.