Est.
React NativeLong read

Native Modules in React Native for iOS Hardware and Sensor Access

TurboModules are now the only supported path for accessing iOS hardware from React Native.

Staff Writer · · 10 min read
Cover illustration for “Native Modules in React Native for iOS Hardware and Sensor Access”
React Native · October 6, 2026 · 10 min read · 2,191 words

JavaScript runs on its own thread. The sensors, radios, and frameworks an iOS app needs, from wireless connectivity to health and motion tracking, live entirely on the native side. This article is about the correct way to make that crossing in 2026: writing a TurboModule, and understanding why the New Architecture changed what that means in practice.

Any React Native app doing serious sensor work, accelerometer fusion, proximity detection, Bluetooth LE peripheral scanning, on-device ML inference, already contains native code, whether a team wrote it themselves or pulled it from a community package. The library react-native-sensor-manager is a representative example: it wraps the accelerometer, gyroscope, magnetometer, orientation sensor, step counter, thermometer, light sensor, and proximity sensor, and exposes all of them to JavaScript through an event-emitter pattern. Someone, somewhere, wrote native code so that JavaScript could ask a question the operating system has to answer. The question a team needs to settle was never whether to write native code. It was which mechanism to use to expose it, and that question now has one answer, because the old mechanism has been retired.

What the New Architecture actually replaced and why that replacement is now mandatory

The New Architecture is built from four parts that depend on each other. JSI, the JavaScript Interface, lets JavaScript call native code directly and synchronously, with no serialization step in between. Fabric moves layout calculation into a shared C++ layer so the UI tree doesn't have to pass back and forth across a bridge. TurboModules add lazy loading, so a native module gets instantiated only the first time something actually calls it. Codegen generates type-safe native bindings automatically from TypeScript or Flow specifications, giving you a checked contract between JavaScript and native code.

That lazy loading detail is not cosmetic. Under the old bridge architecture, every native module an app included, the camera, the file system, an analytics SDK, had to be registered and initialized at app startup, regardless of whether the user ever touched that feature during the session. TurboModules remove that tax: a module only pays its initialization cost the moment it's first called.

The timeline matters because it sets a hard deadline. React Native 0.76 made the New Architecture the default for new projects. React Native 0.84 made the Hermes V1 engine the default JavaScript engine on both iOS and Android, after an initial experimental opt-in in 0.82, and it replaced the legacy Hermes engine that had itself been the default since 0.70. Hermes V1 runs alongside the New Architecture, and it contributes directly to the startup and bytecode-compilation gains teams have measured since the switch. None of this is a roadmap item anymore. It's shipped, and it's the default.

That leaves a practical obligation for any team still running third-party native modules: audit every dependency for TurboModule and Fabric compatibility before shipping. A library that hasn't migrated is a dependency that can break in production the moment the New Architecture assumptions it was never built for catch up with it.

TurboModules as the Center of Gravity for Sensor Work

You can build a native module on iOS in four ways today, and they sit at different points on a line running from high abstraction to low-level control.

The Expo Modules API sits at the top of that line. It's the right choice for modules with simple API surfaces and a need for broad compatibility across the Expo ecosystem, and it asks the least of the developer in exchange for giving up some control.

TurboModules sit at the center, and that center is not accidental: Meta built the New Architecture around them. A TurboModule carries a type-safe cross-language contract generated by Codegen, direct JSI integration, and lazy instantiation, and it does all three without forcing you down into C++. For sensor and hardware work specifically, that combination is close to a design requirement. Sensor data streams continuously and often at high frequency. A malformed payload that would be a minor, recoverable error in a single one-shot API call becomes a real reliability risk when it's happening dozens of times a second inside an accelerometer feed or a Bluetooth LE characteristic notification. When Codegen enforces type checking at the boundary, it catches exactly the class of error that would otherwise surface as an intermittent crash three weeks after launch.

Objective-C runtime bridges are the legacy path, the mechanism teams used before TurboModules existed. They still function, and in isolated compatibility situations they may be the only option available for a given library, but they are architecturally out of step with how the New Architecture is built, and treating them as a forward-looking choice for new work is a mistake.

Pure C++ JSI modules sit at the bottom of the abstraction scale, built for raw computational performance. They cannot talk to mobile hardware directly. To reach an actual sensor from a C++ module on iOS, the code has to make an internal jump through the Objective-C runtime to reach the underlying OS APIs anyway, which adds a layer of friction without buying anything back for hardware-access work. For sensor and hardware integration, that path is strictly worse than going straight to a TurboModule.

Writing the iOS Side of a TurboModule in Swift

Swift is Apple's official and default language for iOS development, and you can write TurboModules in it, but the interoperability between Swift and the C++ core of React Native isn't seamless. The practical pattern is an adapter: the real business logic lives in a Swift class, and a thin Objective-C++ layer connects that Swift object to the React Native runtime.

Getting the project to see Swift at all starts in Xcode's Build Settings, under "Objective-C Bridging Header," where the path needs to point at the bridging header file. Under the legacy bridge, a companion .m file using the RCT_EXTERN_MODULE and RCT_EXTERN_METHOD macros was required to expose Swift methods to the JavaScript side. Under the New Architecture, default since React Native 0.76, that role is handled instead by a .mm Objective-C++ file working with JSI-based TurboModules. The Swift class itself needs the @objc attribute, both on the class declaration and on each method meant to be callable from JavaScript, so the Objective-C runtime can see and export them correctly.

Threading is where hardware modules succeed or fail. Returning false from requiresMainQueueSetup() tells React Native it's safe to initialize the module on a background thread, which is the right answer for sensor and Bluetooth work that never touches UIKit directly. CPU-intensive sensor processing belongs on DispatchQueue.global(qos:.userInitiated), kept off the JavaScript thread so a burst of accelerometer data doesn't stall the UI. Anything that eventually updates the interface has to be dispatched back to DispatchQueue.main before the promise resolver or event emitter fires. The authentication module pattern shows this directly: its authentication callbacks explicitly hop back to the main queue before resolving, because the result of a biometric check is about to touch the UI and cannot be handed back from a background thread.

Choosing between a promise and an event emitter comes down to whether the hardware produces one answer or a stream of them. A one-shot read, a biometric authentication result, a single HealthKit query, belongs behind RCTPromiseResolveBlock and RCTPromiseRejectBlock. A continuous stream, accelerometer data, gyroscope data, magnetometer data, proximity state, Bluetooth LE characteristic notifications, belongs behind an event emitter built by subclassing RCTEventEmitter, because all of these push data on their own schedule.

Testing the proximity sensor requires a physical device, since an emulator cannot simulate it. Verifying that code path requires a physical device, which matters for any distributed team where not every engineer has one sitting on their desk at the moment they need it.

What changes when the hardware API is HealthKit, Core ML, or Bluetooth LE

The TurboModule pattern holds across HealthKit, Core ML, and Bluetooth LE, but each framework adds its own entitlement, permission, or data-flow requirement that a module has to account for.

You need to add HealthKit's entitlement in Xcode's Signing & Capabilities panel before anything else happens. Skip it, and the app doesn't degrade gracefully. It crashes at runtime as soon as a HealthKit API gets called, whether that's a request for authorization or an actual query. Authorization itself is asynchronous and granted per data type, so the module has to request exactly the types it needs, return that result to JavaScript as a promise, and hold off on running any queries until authorization comes back confirmed. HealthKit queries also return on arbitrary background queues chosen by the system, not queues the module controls, so the code has to correctly marshal those results back to whatever completion path JavaScript is waiting on.

Core ML's main constraint is computational weight. Inference is CPU- and memory-intensive, and it needs to run on a background queue, the same DispatchQueue.global(qos: .userInitiated) pattern used for heavy image processing in the ImageProcessor native module pattern. The compiled model file ships bundled inside the iOS app target and gets referenced by name, so if you want efficiency, you load and cache that model once at module initialization instead of reloading it on every single inference call.

Bluetooth LE is largely a delegate-management problem. CBCentralManager requires a delegate to receive its callbacks, and the TurboModule class can serve as that delegate directly, translating each delegate callback into an event emitter call that reaches JavaScript. Connection state, powered on, scanning, connected, disconnected, has to be surfaced through events rather than a single promise response, since a peripheral's state can change at any point during a session, not just at the moment JavaScript asks for it. On top of that, the NSBluetoothAlwaysUsageDescription key has to be present in Info.plist, or the app gets rejected during App Store review.

Across all three frameworks, the TurboModule's job is the same: it sits at the seam between an asynchronous, permission-gated iOS API and the promise or event-emitter surface JavaScript expects. Getting that seam right takes an understanding of both sides of it, not just familiarity with the JavaScript-facing API.

Native vs. React Native Performance for Hardware-Heavy Apps

For a standard business app, screens, forms, network calls, list views, the performance gap between React Native and native Swift has narrowed considerably. That gap has not closed for hardware-intensive, real-time, or graphics-heavy work, and native development remains the stronger choice for heavy graphics, AR and VR, deep hardware integration, real-time media, and platform-exclusive capabilities.

The New Architecture's JSI-enabled synchronous calls cut communication latency substantially, which matters directly for sensor polling. If you don't manage the JavaScript garbage collector carefully, it can still introduce energy inefficiencies, and a high-frequency sensor stream, dozens of accelerometer readings a second, magnifies that risk.

For an app that mainly needs one or two hardware integrations layered onto an otherwise standard UI, a fitness app reading HealthKit step counts, a retail app scanning for Bluetooth LE beacons, a carefully written TurboModule inside a React Native shell is a legitimate production architecture. Shopify's React Native implementation reached greater than 99.9% crash-free sessions in production, with P75 screen loads under 500 milliseconds at scale, which is evidence that the architecture holds up at consumer-grade reliability and speed when the hardware need is bounded.

The calculation changes when the hardware is the product itself: an AR or VR experience, a professional audio tool, an app built around Core ML inference running at the edge. Every team building in this territory eventually runs into a need for some specific, low-level platform feature that no library has wrapped yet, and the bespoke native work required to solve it undercuts the original argument for going cross-platform.

None of this excuses sloppy engineering on the native side. If a native app has blocking network calls, a poorly designed data model, or careless memory management, it will underperform a carefully built React Native app every time. Architecture choice is not a substitute for discipline on either path, and the question a team actually needs to answer is how much of the app's differentiation lives in the hardware layer. The higher that fraction runs, the stronger the case for building native in Swift from the start.

On-Device AI, Foundation Models, and the Calculus for Hardware Modules

Apple's Foundation Models framework gives Swift developers direct access to guided generation, constrained tool calling, and LoRA adapter fine-tuning, letting an app run large language model inference entirely on-device. The framework also introduces @Generable for structured output, so a model's response can come back as a typed Swift value instead of raw text that needs to be parsed and hoped over.

That capability is reachable today only in native Swift. React Native wrappers for frameworks like this one trail the native release by design, since someone has to build and test the bridging layer after the native API ships, not before. A team that wants on-device LLM inference the moment a new iOS AI feature launches cannot wait for a cross-platform wrapper to catch up. The pattern matches everything the rest of this piece has argued: when a capability sits closest to the hardware and the OS, native Swift gets there first, and a TurboModule is, at best, a bridge a team builds once Apple's own surface has stabilized enough to wrap.

Sources

  1. React Native’s 2026 New Architecture: How JSI and Fabric Finally Killed the Performance Bridge
  2. Native Modules: Introduction · React Native
  3. iOS Native Modules · React Native
  4. iOS - Using Swift in Your Native Modules · React Native
  5. React Native in 2026: The Bridge is Burnt (And That’s a Good Thing)
Filed underReact Native

More in React Native