React Native BLE State Restoration on iOS for IoT Apps

BLE feels solid until iOS kills your app. Then the sensor is still there, the user expects a reconnect, and your JavaScript state is gone.
For React Native BLE apps, the fix starts below JavaScript. Core Bluetooth can restore scan and connection work after termination, but only if native iOS owns that state first. Once that clicks, the rest of the architecture gets much simpler.
Why state restoration begins in native iOS
React Native sits above Core Bluetooth. iOS does not relaunch your app because a JS listener existed. It relaunches because a CBCentralManager had a restoration identifier and the system had BLE work worth restoring.
That means centralManager(_:willRestoreState:) can fire before the React bridge is ready. If your whole reconnect story lives in hooks, context, or Redux, you will miss the first and most important event.
iOS restores native BLE objects. It does not restore your React state, in-memory cache, or screen tree.
As of April 2026, there are no widely reported iOS 19-specific changes to this flow. Libraries still depend on Core Bluetooth, so Apple's restoration rules still control background relaunch and restored state. You can inspect Bluetooth launch options during app boot, but treat them as a hint. willRestoreState is still the source of truth because it gives you the restored native objects. If the app was idle, iOS has nothing to restore.
A good mental model is simple: iOS restores the central manager, then your native layer serializes that state, then JavaScript rehydrates from it. If you need a refresher on manager restoration during app startup, this CBCentralManager restoration discussion is still useful.
| State | iOS can restore | You still rebuild | | ---------------------------- | --------------- | ---------------------- | | Active scan | Sometimes | JS intent and filters | | Pending connect | Yes | Reconnect policy | | Connected peripheral refs | Yes | Device session model | | Characteristic subscriptions | Partly | App logic and UI state | | Cached React data | No | Everything in JS |
The table is the part many teams miss. Restoration gives you native handles, not a finished app state.
A setup that survives termination and background relaunch
Start with Info.plist. Add UIBackgroundModes with bluetooth-central. Then create one long-lived central manager in native iOS, usually in AppDelegate, a singleton, or a dedicated BLE coordinator. Pass CBCentralManagerOptionRestoreIdentifierKey with a stable value such as com.yourapp.ble.central.
Next, keep that native owner alive for the whole app. One central per screen is a trap. Restoration works best when a single coordinator owns scanning, connection attempts, peripheral maps, and pending events.
A small native event buffer solves most timing bugs. Keep an array of normalized events, write it before touching JS, and expose methods such as getPendingBleState() and clearPendingBleState() through a module or TurboModule.
In Swift, the important pieces look like this in practice: create `CBCentralManager(delegate: self, queue: bleQueue, options: [CBCentralManagerOptionRestoreIdentifierKey: "com.yourapp.ble.central"]), implement centralManager(_:willRestoreState:), store restored peripherals by identifier.uuidString`, and buffer a normalized payload for JavaScript.
If you use react-native-ble-plx, wire the JS side once, near app bootstrap, with restoreStateIdentifier and restoreStateFunction, as described in the ble-plx background mode discussion. Still, treat that as the second half of the design, not the first. JS can consume restored data, but native code must catch it early.
A reliable flow usually has four steps:
- Native receives
willRestoreStateand saves peripheral IDs, connect state, and scan intent. - Native emits an event only if the bridge is ready. Otherwise, it queues the payload.
- JavaScript boots, asks for pending BLE restoration data, and rehydrates device models.
- JS decides whether to reconnect, resubscribe, or mark the device as recovering.
This is where a custom native module, or a small library fork, becomes necessary. If your library hides willRestoreState, creates duplicate central instances, or drops events before JS loads, React Native-only code won't fix it.
Reconnect flows, scan restoration, and the caveats that bite
The app lifecycle matters more than most BLE APIs. When the app is backgrounded and later terminated by the system, iOS may relaunch it for an active connection or a pending connect. In that case, your native layer should map restored CBPeripheral objects to your saved device records, then decide whether to call connect, discoverServices, or wait for the current state callback.
After reconnect, many IoT apps also need to rediscover services and reattach characteristic notifications. Don't assume old subscriptions survive termination or radio loss, even when the peripheral reference was restored.
Scanning is trickier. iOS may restore a scan, but background discovery is limited and filtered. You should not promise "always discover nearby devices after termination" unless you've tested the exact advertising pattern, service UUIDs, and wake conditions. This background restoration thread shows how often scan-based relaunch assumptions fail.
Reboot is even stricter. After a phone restart, auto-relaunch is not something you should treat as guaranteed behavior for central apps. A recent reboot relaunch question is a good reminder to design for missed wakes.
Duplicate peripheral instances cause another common bug. Core Bluetooth may hand you a restored peripheral object that does not match the object reference you had before termination. Compare by identifier.uuidString, not by instance identity. The same rule should flow into JS. Device IDs are stable, object references are not.
Use this checklist when restoration looks broken:
- Confirm
bluetooth-centralis inInfo.plistand the central manager has a restore identifier. - Verify the app had active scan or connect work before termination.
- Make sure only one native central manager exists.
- Deduplicate peripherals by UUID in native code and in JavaScript.
- Queue restored events until the React bridge reports ready.
- Re-test with real service UUID filters, not broad scans.
- Test termination, background wake, and device reboot as separate cases.
If there's one rule to keep, keep this one: iOS restores native BLE state, not your React app. Capture that native state early, normalize it, and replay it into JavaScript once the bridge is alive.
That architecture takes more work up front. In return, your IoT app reconnects more predictably, background behavior makes sense, and BLE bugs get a lot easier to reason about.