mobile-react-native-native-modules

Accessing iOS-specific APIs not available in React Native

React Native Native Modules

Scope: Native modules, bridging Swift/Objective-C, iOS APIs, TurboModules, native UI components Lines: ~350 Last Updated: 2025-10-18

When to Use This Skill

Activate this skill when:

Core Concepts

Native Module Architecture

Bridge Pattern:

TurboModules (New Architecture):

Communication Flow:

JavaScript → Bridge → Native Module → iOS API
iOS API → Native Module → Bridge → JavaScript

Swift Bridging

Bridging Header (Objective-C interop):

@objc Attribute:

Event Emitters

Push vs Pull:

Use Cases:


Patterns

Pattern 1: Basic Swift Native Module

// ios/MyModule.swift
import Foundation
import React

@objc(MyModule)
class MyModule: NSObject {

  // MARK: - React Native Export

  @objc
  static func requiresMainQueueSetup() -> Bool {
    return false // Run on background thread
  }

  @objc
  func greet(_ name: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
    let greeting = "Hello, \(name)!"
    resolver(greeting)
  }

  @objc
  func calculateSum(_ a: NSNumber, b: NSNumber, callback: @escaping RCTResponseSenderBlock) {
    let sum = a.doubleValue + b.doubleValue
    callback([NSNull(), sum]) // [error, result] pattern
  }

  @objc
  func getDeviceInfo() -> NSDictionary {
    return [
      "model": UIDevice.current.model,
      "systemVersion": UIDevice.current.systemVersion,
      "name": UIDevice.current.name
    ]
  }
}
// ios/MyModule.m (Bridging file)
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(MyModule, NSObject)

RCT_EXTERN_METHOD(greet:(NSString *)name
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(calculateSum:(nonnull NSNumber *)a
                  b:(nonnull NSNumber *)b
                  callback:(RCTResponseSenderBlock)callback)

RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(getDeviceInfo)

@end
// src/modules/MyModule.ts
import { NativeModules } from 'react-native';

interface MyModuleInterface {
  greet(name: string): Promise<string>;
  calculateSum(a: number, b: number, callback: (error: any, result: number) => void): void;
  getDeviceInfo(): { model: string; systemVersion: string; name: string };
}

const { MyModule } = NativeModules;

export default MyModule as MyModuleInterface;

Benefits:

Pattern 2: Event Emitter Native Module

// ios/SensorModule.swift
import Foundation
import CoreMotion
import React

@objc(SensorModule)
class SensorModule: RCTEventEmitter {

  private var motionManager: CMMotionManager?
  private var isObserving = false

  override init() {
    super.init()
    motionManager = CMMotionManager()
  }

  // MARK: - React Native Export

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

  override func supportedEvents() -> [String]! {
    return ["onAccelerometerData", "onDeviceMotion"]
  }

  @objc
  func startAccelerometer() {
    guard let manager = motionManager, !isObserving else { return }

    isObserving = true
    manager.accelerometerUpdateInterval = 0.1

    manager.startAccelerometerUpdates(to: .main) { [weak self] (data, error) in
      guard let data = data, error == nil else { return }

      self?.sendEvent(withName: "onAccelerometerData", body: [
        "x": data.acceleration.x,
        "y": data.acceleration.y,
        "z": data.acceleration.z,
        "timestamp": Date().timeIntervalSince1970
      ])
    }
  }

  @objc
  func stopAccelerometer() {
    motionManager?.stopAccelerometerUpdates()
    isObserving = false
  }

  override func stopObserving() {
    stopAccelerometer()
  }
}
// ios/SensorModule.m
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface RCT_EXTERN_MODULE(SensorModule, RCTEventEmitter)

RCT_EXTERN_METHOD(startAccelerometer)
RCT_EXTERN_METHOD(stopAccelerometer)

@end
// src/modules/SensorModule.ts
import { NativeEventEmitter, NativeModules } from 'react-native';

const { SensorModule } = NativeModules;
const sensorEmitter = new NativeEventEmitter(SensorModule);

export interface AccelerometerData {
  x: number;
  y: number;
  z: number;
  timestamp: number;
}

export const startAccelerometer = () => {
  SensorModule.startAccelerometer();
};

export const stopAccelerometer = () => {
  SensorModule.stopAccelerometer();
};

export const subscribeToAccelerometer = (
  callback: (data: AccelerometerData) => void
) => {
  const subscription = sensorEmitter.addListener('onAccelerometerData', callback);
  return () => subscription.remove();
};

When to use:

Pattern 3: Native UI Component (Swift)

// ios/CircularProgressView.swift
import UIKit

class CircularProgressView: UIView {

  private var progressLayer = CAShapeLayer()
  private var trackLayer = CAShapeLayer()

  @objc var progress: CGFloat = 0 {
    didSet {
      updateProgress()
    }
  }

  @objc var progressColor: UIColor = .systemBlue {
    didSet {
      progressLayer.strokeColor = progressColor.cgColor
    }
  }

  @objc var trackColor: UIColor = .systemGray5 {
    didSet {
      trackLayer.strokeColor = trackColor.cgColor
    }
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    setupLayers()
  }

  private func setupLayers() {
    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    let radius = min(bounds.width, bounds.height) / 2 - 10
    let circularPath = UIBezierPath(
      arcCenter: center,
      radius: radius,
      startAngle: -.pi / 2,
      endAngle: 3 * .pi / 2,
      clockwise: true
    )

    // Track layer
    trackLayer.path = circularPath.cgPath
    trackLayer.fillColor = UIColor.clear.cgColor
    trackLayer.strokeColor = trackColor.cgColor
    trackLayer.lineWidth = 10
    trackLayer.lineCap = .round

    // Progress layer
    progressLayer.path = circularPath.cgPath
    progressLayer.fillColor = UIColor.clear.cgColor
    progressLayer.strokeColor = progressColor.cgColor
    progressLayer.lineWidth = 10
    progressLayer.lineCap = .round
    progressLayer.strokeEnd = 0

    layer.addSublayer(trackLayer)
    layer.addSublayer(progressLayer)
  }

  private func updateProgress() {
    CATransaction.begin()
    CATransaction.setAnimationDuration(0.3)
    progressLayer.strokeEnd = progress
    CATransaction.commit()
  }
}
// ios/CircularProgressViewManager.swift
import React

@objc(CircularProgressViewManager)
class CircularProgressViewManager: RCTViewManager {

  override static func requiresMainQueueSetup() -> Bool {
    return true // UI components need main thread
  }

  override func view() -> UIView! {
    return CircularProgressView()
  }
}
// ios/CircularProgressViewManager.m
#import <React/RCTViewManager.h>

@interface RCT_EXTERN_MODULE(CircularProgressViewManager, RCTViewManager)

RCT_EXPORT_VIEW_PROPERTY(progress, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(progressColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(trackColor, UIColor)

@end
// src/components/CircularProgress.tsx
import React from 'react';
import { requireNativeComponent, ViewProps } from 'react-native';

interface CircularProgressProps extends ViewProps {
  progress: number; // 0.0 - 1.0
  progressColor?: string;
  trackColor?: string;
}

const NativeCircularProgress = requireNativeComponent<CircularProgressProps>(
  'CircularProgressView'
);

export const CircularProgress: React.FC<CircularProgressProps> = (props) => {
  return <NativeCircularProgress {...props} />;
};

Benefits:

Pattern 4: Async Native Operations with Error Handling

// ios/NetworkModule.swift
import Foundation
import React

@objc(NetworkModule)
class NetworkModule: NSObject {

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

  @objc
  func fetchUser(_ userId: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {

    guard let url = URL(string: "https://api.example.com/users/\(userId)") else {
      rejecter("INVALID_URL", "Invalid user ID", nil)
      return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
      // Handle network error
      if let error = error {
        rejecter("NETWORK_ERROR", error.localizedDescription, error)
        return
      }

      // Handle HTTP error
      guard let httpResponse = response as? HTTPURLResponse else {
        rejecter("INVALID_RESPONSE", "Invalid response type", nil)
        return
      }

      guard (200...299).contains(httpResponse.statusCode) else {
        rejecter("HTTP_ERROR", "Status code: \(httpResponse.statusCode)", nil)
        return
      }

      // Parse JSON
      guard let data = data else {
        rejecter("NO_DATA", "No data received", nil)
        return
      }

      do {
        let json = try JSONSerialization.jsonObject(with: data, options: [])
        resolver(json)
      } catch {
        rejecter("PARSE_ERROR", "Failed to parse JSON", error)
      }
    }.resume()
  }
}
// src/modules/NetworkModule.ts
import { NativeModules } from 'react-native';

interface User {
  id: string;
  name: string;
  email: string;
}

class NetworkModuleWrapper {
  async fetchUser(userId: string): Promise<User> {
    try {
      const user = await NativeModules.NetworkModule.fetchUser(userId);
      return user as User;
    } catch (error: any) {
      // Error codes from native module
      switch (error.code) {
        case 'INVALID_URL':
          throw new Error('Invalid user ID format');
        case 'NETWORK_ERROR':
          throw new Error('Network connection failed');
        case 'HTTP_ERROR':
          throw new Error(`Server error: ${error.message}`);
        case 'PARSE_ERROR':
          throw new Error('Invalid response format');
        default:
          throw error;
      }
    }
  }
}

export default new NetworkModuleWrapper();

When to use:

Pattern 5: Swift Concurrency Integration

// ios/ModernAsyncModule.swift
import Foundation
import React

@objc(ModernAsyncModule)
class ModernAsyncModule: NSObject {

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

  @objc
  func performAsyncWork(_ input: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {

    Task {
      do {
        let result = try await processData(input)
        resolver(result)
      } catch {
        rejecter("ASYNC_ERROR", error.localizedDescription, error)
      }
    }
  }

  private func processData(_ input: String) async throws -> [String: Any] {
    // Simulate async work
    try await Task.sleep(nanoseconds: 1_000_000_000)

    // Use structured concurrency
    async let part1 = fetchPart1(input)
    async let part2 = fetchPart2(input)

    let results = try await [part1, part2]

    return [
      "input": input,
      "results": results,
      "timestamp": Date().timeIntervalSince1970
    ]
  }

  private func fetchPart1(_ input: String) async throws -> String {
    try await Task.sleep(nanoseconds: 500_000_000)
    return "Part1: \(input)"
  }

  private func fetchPart2(_ input: String) async throws -> String {
    try await Task.sleep(nanoseconds: 500_000_000)
    return "Part2: \(input)"
  }
}

Benefits:

Pattern 6: Objective-C Native Module (Legacy)

// ios/LegacyModule.h
#import <React/RCTBridgeModule.h>

@interface LegacyModule : NSObject <RCTBridgeModule>
@end
// ios/LegacyModule.m
#import "LegacyModule.h"

@implementation LegacyModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(addNumbers:(NSInteger)a
                  b:(NSInteger)b
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  NSInteger sum = a + b;
  resolve(@(sum));
}

RCT_EXPORT_METHOD(performCallback:(RCTResponseSenderBlock)callback)
{
  callback(@[[NSNull null], @"Success"]);
}

+ (BOOL)requiresMainQueueSetup
{
  return NO;
}

@end

When to use:


Quick Reference

Bridge Data Types

JavaScript Type    | Objective-C/Swift Type       | Notes
-------------------|------------------------------|------------------
number             | NSNumber / Double / Int      | Auto-converted
string             | NSString / String            | UTF-8 encoded
boolean            | NSNumber / Bool              | true/false
null/undefined     | NSNull / nil                 | Check for NSNull
Array              | NSArray / [Any]              | Ordered collection
Object             | NSDictionary / [String: Any] | Key-value pairs
Function           | RCTResponseSenderBlock       | Callback
Promise            | RCTPromise(Resolve/Reject)   | Async result

Module Export Macros

RCT_EXPORT_MODULE()                      // Export module with class name
RCT_EXPORT_MODULE(CustomName)            // Export with custom name
RCT_EXPORT_METHOD(methodName:...)        // Export async method
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD() // Export synchronous method
RCT_EXPORT_VIEW_PROPERTY(name, type)     // Export view property

Best Practices

✅ DO: Use promises for async operations
✅ DO: Validate input parameters
✅ DO: Return descriptive error codes
✅ DO: Run on background thread when possible
✅ DO: Use type-safe TypeScript interfaces

❌ DON'T: Block the main thread with heavy operations
❌ DON'T: Return functions or callbacks from native
❌ DON'T: Store references to JavaScript objects
❌ DON'T: Use synchronous methods for slow operations
❌ DON'T: Forget to handle errors properly

Anti-Patterns

Synchronous heavy operations: Blocking main thread with slow native code ✅ Use promises and background threads: requiresMainQueueSetup() -> false

Over-bridging: Calling native for simple logic better done in JS ✅ Use native only for platform APIs, performance-critical code, or third-party SDKs

No error handling: Native errors crash the app ✅ Always use try-catch and reject promises with descriptive error codes

Storing JavaScript callbacks: Memory leaks and crashes ✅ Use event emitters for continuous updates, promises for one-time results

Large data serialization: Sending MB of data across bridge ✅ Use file paths or shared memory, send only metadata

Main thread for async work: UI freezes during operations ✅ Use background threads for network, file I/O, computations

Missing TypeScript types: Untyped native module usage ✅ Create interfaces for all native modules with full type safety

Not cleaning up resources: Memory leaks from sensors, timers, etc. ✅ Implement stopObserving and cleanup in dealloc/deinit


Related Skills


Last Updated: 2025-10-18 Format Version: 1.0 (Atomic)