The short answer
Almost every connected hardware product reaches the same fork: one codebase for both platforms, or two native apps. React Native with BLE works, and works well, for the majority of companion apps — but the problems you will hit are almost never in the JavaScript layer. They come from operating system behavior, and no cross-platform framework insulates you from that. The decision therefore depends far less on React Native than on how demanding your Bluetooth use case is.
What works without special effort
For the common pattern — scan for devices, connect, read and write GATT characteristics, subscribe to notifications, show live values — the mature community BLE modules do a solid job. Environmental sensors, smart locks, measurement instruments, lighting controllers, and battery-powered trackers are all routinely built this way.
Throughput is rarely the limitation people expect. The bottleneck is the radio protocol itself, not the bridge to JavaScript. A sensor reporting once a second, or even ten times a second, is nowhere near the ceiling. Onboarding flows — permission prompts, discovery, pairing, the first screen — are also entirely buildable in shared code, and designing that flow well matters more to your reviews than the framework does. Background on the connection model is in what a BLE app costs to build, and network setup for Wi-Fi products is covered in Wi-Fi provisioning from an app.
Where the pain actually starts
- Background operation. Each platform restricts scanning and connecting differently when the app is not in the foreground: one requires declaring background modes and implementing state restoration, the other requires a foreground service and fights aggressive battery optimization from device makers. This is configured in the native project, not in shared JavaScript, and it is the single most common source of "it works on my phone" bugs.
- Large transfers. Firmware updates over the air and pulling long measurement logs generate high message volume across the bridge. It is workable, but a serious over-the-air update path usually deserves a native module — see OTA firmware updates.
- Connection state management. Random disconnects, concurrent requests, retry and reconnect logic, and multi-device sessions require a properly designed state machine with a request queue. No library does this for you, and skipping it produces an app that feels unreliable even when the firmware is fine.
- Permissions. The permission model has changed repeatedly across OS versions, including whether location access is implied by scanning. Expect real work here, and expect it to need revisiting.
- Pairing, bonding, and encryption. If your product requires bonded, encrypted connections, behavior diverges sharply between platforms and cross-platform abstractions get thin.
- Device fragmentation. Android Bluetooth stacks differ by chipset and vendor. A physical test matrix of real handsets is not optional.
When to go native instead
- Sustained high-rate streaming — continuous audio, waveform data, or high-frequency motion capture.
- Background reliability is the product — a monitor that must keep logging while the phone is in a pocket for hours.
- Regulated or safety-critical use. Medical and industrial products with verification and validation obligations often justify native implementation for traceability alone.
- Classic Bluetooth or audio profiles, or Apple accessory programs that impose their own requirements — see Apple MFi certification.
- Deep platform integrations like widgets, complications, or wearable companions.
The architecture that works in practice
The pattern we recommend to clients is a hybrid, not a purity contest:
- Write the BLE layer as a native module on each platform — connection lifecycle, queue, retries, background behavior — and expose one clean, well-documented interface to JavaScript.
- Build everything above it in shared code: screens, state, charts, account handling, cloud sync. That is where most of your app's code and most of your future changes live.
- Write a GATT contract document that firmware and app both sign off on: services, characteristic UUIDs, data formats, byte order, units, error codes, and expected timing. More companion-app schedule slips trace to a vague GATT spec than to any framework choice.
- Build a simulator so app work is not blocked by hardware availability, and keep a small farm of real test devices for the behaviors a simulator cannot reproduce.
Cost and schedule implications
Shared code genuinely reduces the cost of the app's user interface, which is where most screens and iterations happen. It does not reduce the cost of the Bluetooth layer, background behavior, or device testing — those are roughly the same work either way, and they are frequently the larger half of a hardware companion app. Founders who budget as if one codebase halves the total are the ones who get surprised. See mobile app development cost and the wider tradeoff in native vs cross-platform development. If you are comparing frameworks specifically, the same analysis applies to using Flutter for a companion app.
Build the connection layer properly
Projects House develops hardware and companion apps together, which means the GATT specification, the firmware, and the app's state machine are designed as one system rather than negotiated across two vendors. If you are choosing a stack for a connected product, describe it through our contact form and we will tell you where the real risk sits.