reliably pairing with IoT devices
A React Native app that talks to an IoT device over Bluetooth Low Energy has to handle scanning, pairing, permissions, and dropped connections robustly. This article walks through a complete Bluetooth integration with react-native-ble-plx, from the GATT hierarchy to production-grade reconnect logic.
Table of Contents
- 1. Why Bluetooth Low Energy is the foundation for IoT pairing
- 2. Library choice: react-native-ble-plx vs. react-native-ble-manager
- 3. Permissions: iOS Info.plist and Android runtime rights
- 4. Scanning for devices: filters, duplicates, and battery impact
- 5. Establishing a connection: understanding the GATT hierarchy
- 6. Reading, writing, and subscribing to characteristics
- 7. Connection state: reconnect strategies and MTU
- 8. Bluetooth in the background: iOS vs. Android
- 9. Error handling and debugging in practice
- 10. Summary
- 11. FAQ
1. Why Bluetooth Low Energy is the foundation for IoT pairing
Every React Native Bluetooth integration with IoT hardware starts with a decision that is rarely questioned: Bluetooth Low Energy (BLE) instead of classic Bluetooth. BLE was purpose-built for devices with small batteries. A heart rate sensor, a smart lock, or a temperature probe sends its data in short, energy-efficient bursts instead of maintaining a permanent high-bandwidth connection. For a Bluetooth integration in a mobile app, this means you are not talking to a stream, you are talking to a structured data model made of services and characteristics.
The GATT profile (Generic Attribute Profile) is the heart of every BLE communication. Each peripheral advertises one or more services, and each service groups characteristics with clearly defined UUIDs. A Bluetooth integration that ignores this model and tries to treat BLE like a classic socket connection regularly runs into timing problems and inconsistent states.
React Native has no native BLE API in its core, so every React Native Bluetooth solution runs through a community library that provides a native bridge to Core Bluetooth on iOS and the Android BluetoothLeScanner API on Android. That makes picking the right library the first critical step in any implementation.
2. Library choice: react-native-ble-plx vs. react-native-ble-manager
For a production-ready Bluetooth integration, two established libraries are worth considering. react-native-ble-plx builds on RxJS-style observables and offers a consistent Promise-based API for scanning, connecting, and characteristic operations. react-native-ble-manager, on the other hand, works with events through a NativeEventEmitter, which means more manual wiring but stays a bit closer to the native API.
In practice, react-native-ble-plx wins on maintainability: connection state, errors, and characteristic updates can be subscribed to as streams, which makes reconnect logic and state management significantly simpler. react-native-ble-manager, by contrast, tends to shine on very old React Native versions or when migrating from an existing native Bluetooth stack where the event-based mindset is already established.
Both libraries require native linking and a rebuild of the iOS and Android projects, a plain Expo Go workflow without a custom dev client will not work. For any new React Native Bluetooth app, a development build instead of Expo Go is therefore the right choice from day one.
// hooks/useBleDevice.js
import { useEffect, useRef, useState } from 'react';
import { BleManager } from 'react-native-ble-plx';
const manager = new BleManager();
const IOT_SERVICE_UUID = '0000181a-0000-1000-8000-00805f9b34fb';
export function useBleScan() {
const [devices, setDevices] = useState([]);
const subscription = useRef(null);
useEffect(() => {
// Start scanning, filtered by our IoT service UUID to reduce noise
manager.startDeviceScan([IOT_SERVICE_UUID], { allowDuplicates: false }, (error, device) => {
if (error) {
console.warn('Scan error:', error.message);
return;
}
if (device) {
setDevices((prev) => {
const exists = prev.find((d) => d.id === device.id);
return exists ? prev : [...prev, device];
});
}
});
return () => manager.stopDeviceScan();
}, []);
return devices;
}
3. Permissions: iOS Info.plist and Android runtime rights
No Bluetooth integration starts without the right permissions. On iOS, the Info.plist must include the NSBluetoothAlwaysUsageDescription key with a clear, user-facing explanation. Without this entry, the app crashes silently on the first scan call, a behavior that surprises many developers the first time around.
On Android, the permission model changed fundamentally with Android 12. Before Android 12, scanning required ACCESS_FINE_LOCATION, because BLE beacons can theoretically be used to derive location. Since Android 12, there are the granular permissions BLUETOOTH_SCAN and BLUETOOTH_CONNECT, which explicitly do not imply location data as long as the neverForLocation flag is set. An app that needs to support both permission models checks the API level at runtime and requests the appropriate rights.
The runtime request itself must happen before every scan start, not just once at app launch. Users can revoke permissions at any time in system settings, and a robust React Native Bluetooth implementation checks the current permission status before calling startDeviceScan, rather than relying on a one-time check during onboarding.
# Android 12+ manifest permissions for BLE scanning and connecting
# app/src/main/AndroidManifest.xml additions (shown here as setup commands/comments)
# neverForLocation: scan does not derive location, no location permission needed
# <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
# android:usesPermissionFlags="neverForLocation" />
# <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
# Legacy support for API < 31
# <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
# android:maxSdkVersion="30" />
# Rebuild native projects after adding react-native-ble-plx
npx pod-install ios
cd android && ./gradlew clean && cd ..
npx react-native run-android
4. Scanning for devices: filters, duplicates, and battery impact
An unfiltered scan in a densely populated environment returns dozens of unrelated BLE devices, from headphones to smart TVs. A focused Bluetooth integration therefore filters by the service UUID of its own IoT device right at the startDeviceScan call, instead of filtering client-side afterward. That reduces both the volume of advertisement packets to process and the energy cost of the scan itself.
The allowDuplicates flag controls whether repeated advertisement packets from the same device are reported again. For a simple device list it should stay off, but for use cases like a real-time RSSI signal strength indicator it is necessary, at a noticeably higher battery cost. A scan timeout of 10 to 15 seconds is a good practical compromise between hit rate and battery drain.
Continuous background scanning is heavily restricted on both platforms and should be avoided wherever possible. A well-built React Native Bluetooth app scans deliberately in response to user interaction, such as opening a pairing screen, and stops the scan immediately once the target device is found or the timeout elapses.
5. Establishing a connection: understanding the GATT hierarchy
Once a device is found, the actual connection process begins, and this is where the GATT hierarchy proves central to every Bluetooth integration. A peripheral offers one or more services, and each service groups thematically related characteristics. A smart lock, for example, offers a "Lock Service" with a "Lock State" characteristic and an "Unlock Command" characteristic.
The flow is always the same: connectToDevice, then discoverAllServicesAndCharacteristics. Only after this discovery step does the app know the concrete UUIDs it will later read and write. Many bugs in React Native Bluetooth apps stem from discovery and the actual read/write access not being sequenced cleanly, for instance starting a characteristic operation before discovery has finished.
UUIDs should be maintained centrally as constants, ideally matching the manufacturer's firmware documentation. With proprietary IoT hardware, UUIDs often deviate from the Bluetooth SIG standard, which is why the device manufacturer's exact specification is always the most reliable source.
6. Reading, writing, and subscribing to characteristics
Three operations cover the bulk of any Bluetooth integration: reading (readCharacteristicForDevice), writing (writeCharacteristicWithResponseForDevice), and subscribing to notifications (monitorCharacteristicForDevice). All payloads travel across the native bridge as Base64-encoded strings, which requires a conversion to byte arrays in JavaScript before the actual payload can be interpreted.
Notify and indicate characteristics are the most important mechanism for IoT sensor data: instead of actively polling, the app subscribes to a characteristic once and then automatically receives every update the firmware sends. This significantly reduces both radio traffic and latency compared to periodic polling and is the preferred pattern for sensors like temperature or battery level.
Write operations with response confirm receipt by the peripheral and should always be used for critical commands like "unlock the door", while write-without-response is used for high-frequency, non-critical data such as continuous control signals. Choosing the right write mode is a detail many React Native Bluetooth tutorials skip over, but it has a direct impact on the app's perceived reliability.
{
"device": "SmartLock-IoT-4471",
"services": [
{
"uuid": "0000181a-0000-1000-8000-00805f9b34fb",
"name": "Lock Service",
"characteristics": [
{ "uuid": "00002a6e-0000-1000-8000-00805f9b34fb", "name": "Lock State", "properties": ["read", "notify"] },
{ "uuid": "00002a6f-0000-1000-8000-00805f9b34fb", "name": "Unlock Command", "properties": ["writeWithResponse"] }
]
}
],
"mtu": 185
}
7. Connection state: reconnect strategies and MTU
BLE connections drop, that is not the exception, it is the normal case in mobile usage: the user walks away from the device, switches apps, or iOS terminates the connection for resource management reasons. A resilient Bluetooth integration therefore subscribes to connection state via onDeviceDisconnected and implements automatic reconnection with exponential backoff instead of immediate, endless reconnect attempts.
The default BLE MTU (Maximum Transmission Unit) is 23 bytes, of which only 20 bytes are actual payload, a tight limit for larger data packets like firmware updates or configuration objects. An MTU negotiation via requestMTU can raise this limit up to 517 bytes, though the size actually achievable depends on the peripheral's chipset and must always be treated as a negotiation outcome, not a guaranteed value.
Reconnect logic should persist the last known device identifier, so the app tries to reconnect to the most recently paired device directly on the next launch instead of starting a completely new scan. That significantly shortens the perceived wait time for users and is a key quality marker of a mature React Native Bluetooth app.
8. Bluetooth in the background: iOS vs. Android
As soon as the app moves to the background, iOS and Android differ fundamentally in how they handle existing BLE connections. iOS allows an already established connection to persist in the background if the "bluetooth-central" background mode is enabled in the capabilities, but new scans in the background are heavily throttled and only deliver filtered advertisements at larger intervals.
Android requires a foreground service with a visible notification for sustained BLE activity in the background once the system enters Doze mode. A Bluetooth integration that tries to scan or poll continuously in the background without a foreground service will be throttled or fully paused by the system after a short time.
For most IoT use cases, the most pragmatic solution is to deliberately restrict Bluetooth operations to the foreground and actively check whether the connection is still alive when returning to the foreground. That avoids complex platform-specific background workarounds and keeps the React Native Bluetooth logic maintainable.
9. Error handling and debugging in practice
Android device manufacturers implement the Bluetooth stack differently, and that is exactly where the most stubborn bugs in any Bluetooth integration originate. Some OEMs cache GATT discovery aggressively, which means characteristic changes from the firmware go unnoticed until the app explicitly disconnects and reconnects. An explicit refreshGattCache call before re-running discovery fixes this problem on most affected devices.
On iOS, Core Bluetooth offers a state restoration mechanism that lets the app recover connection state after a system-forced termination. react-native-ble-plx only exposes this mechanism to a limited degree, which is why high-availability use cases often need a thin native wrapper that sets the restoreIdentifier configuration directly against CBCentralManager.
| Criterion | react-native-ble-plx | react-native-ble-manager |
|---|---|---|
| API style | Observable/Promise-based | Event-emitter-based |
| Reconnect logic | Simple via streams | Manual event wiring needed |
| MTU negotiation | Built in (requestMTU) | Built in, less documented |
| Maintenance status | Active, strong community | Active, smaller community |
| Recommendation | New projects | Legacy code migration |
// Simplified Core Bluetooth state restoration handling (native wrapper)
// Used to recover connection state after the OS terminates the app in background
func centralManager(_ central: CBCentralManager,
willRestoreState dict: [String: Any]) {
// Restore previously connected peripherals from the restoration dictionary
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] {
for peripheral in peripherals {
peripheral.delegate = self
// Re-discover services once restoration completes
peripheral.discoverServices(nil)
}
}
}
// Refreshing the GATT cache on Android to pick up firmware characteristic changes
// Some OEM Bluetooth stacks cache service discovery aggressively
fun refreshGattCache(gatt: BluetoothGatt): Boolean {
return try {
val method = gatt.javaClass.getMethod("refresh")
method.invoke(gatt) as Boolean
} catch (e: Exception) {
Log.w("BleIntegration", "GATT cache refresh failed", e)
false
}
}
For debugging a React Native Bluetooth connection, a tool like nRF Connect is indispensable: it shows the complete GATT structure of a device independent of your own app and makes it visible whether a problem lies in the app logic or in the firmware. Anyone unfamiliar with this reference implementation often ends up looking for bugs in the wrong place in their own code.
Mironsoft
React Native development for IoT and hardware integration
Bluetooth integration that holds up in the field?
We build React Native apps that pair reliably with BLE hardware, including the permission model, reconnect strategies, and platform-specific background behavior.
BLE architecture
GATT modeling, library selection, and native bridge design
Permission audit
iOS and Android 12 permission model implemented correctly
Reconnect logic
Backoff strategies and state restoration for high availability
10. Summary
A resilient React Native Bluetooth integration lives and dies with understanding the GATT model: services, characteristics, and their properties determine how reading, writing, and subscribing are correctly orchestrated. react-native-ble-plx offers the most consistent API for that, especially for reconnect logic and MTU negotiation. Permissions must be handled separately for iOS and for the granular Android 12 model, and background behavior differs fundamentally between the platforms.
The biggest lever for stability lies in consistently treating dropped connections as the normal case, not the exception. A Bluetooth integration that plans for reconnects with backoff, GATT cache refresh for problematic Android devices, and state restoration on iOS from the start delivers a noticeably more reliable user experience in the field than an implementation that treats BLE as an always-available connection.
React Native Bluetooth Integration: Key Takeaways
Library
react-native-ble-plx for new projects, observable-based API significantly simplifies reconnect logic.
Permissions
iOS NSBluetoothAlwaysUsageDescription, Android BLUETOOTH_SCAN/CONNECT from API 31, ACCESS_FINE_LOCATION before that.
GATT hierarchy
Service to characteristic to descriptor. Always finish discovery before read/write access.
Robustness
Reconnect with backoff, GATT cache refresh for Android OEM quirks, state restoration on iOS.