@beacio/react
v2.1.1
Published
React hooks for Bluetooth Low Energy — useDevice, useScan, useProfile. Real-time BLE data in React
Maintainers
Readme
@beacio/react
React hooks and components for Web Bluetooth. Works with the beacio Safari Extension for iOS support.
Installation
npm install @beacio/react @beacio/coreAdd the polyfill import to your app entry file:
import '@beacio/core/auto';Quick Start
import { BeacioProvider, useBluetooth, useDevice } from '@beacio/react';
import type { BeacioDevice } from '@beacio/core';
function App() {
return (
<BeacioProvider>
<HeartRateMonitor />
</BeacioProvider>
);
}
function HeartRateMonitor() {
const { requestDevice } = useBluetooth();
const [device, setDevice] = useState<BeacioDevice | null>(null);
const { isConnected, connect, disconnect } = useDevice(device, { autoReconnect: true });
const handlePair = async () => {
// Must be called from a user gesture (button click)
const d = await requestDevice({ filters: [{ services: ['heart_rate'] }] });
if (d) setDevice(d);
};
return (
<div>
{!device && <button onClick={handlePair}>Pair</button>}
{device && !isConnected && <button onClick={connect}>Connect</button>}
{isConnected && <button onClick={disconnect}>Disconnect</button>}
</div>
);
}Hooks
useBluetooth()
Main hook for Bluetooth availability and device requests.
import { useBluetooth } from '@beacio/react';
const {
isAvailable, // Web Bluetooth available?
isExtensionInstalled, // beacio extension installed?
requestDevice, // Request device (must be called from user gesture)
getDevices, // Get previously paired devices
ble, // Core beacio instance
backgroundSync, // Background sync API
peripheral, // Peripheral mode API
error,
} = useBluetooth();useDevice(device, options?)
Manage a device's connection lifecycle with optional auto-reconnect.
import { useDevice } from '@beacio/react';
const {
connectionState, // 'disconnected' | 'connecting' | 'connected' | 'disconnecting'
isConnected,
isConnecting,
connect,
disconnect,
services, // Discovered GATT services
error,
autoReconnect, // Current auto-reconnect state
setAutoReconnect, // Toggle auto-reconnect
reconnectAttempt, // Current reconnect attempt number (0 = not reconnecting)
} = useDevice(device, {
autoReconnect: true,
reconnectAttempts: 3,
reconnectDelay: 1000,
reconnectBackoffMultiplier: 2,
onReconnectAttempt: (attempt, delayMs) => {},
onReconnectSuccess: (attempt) => {},
onReconnectFailure: (error, attempt, willRetry) => {},
});useCharacteristic(device, serviceUUID, characteristicUUID)
Read, write, and subscribe to a BLE characteristic. All operations delegate to the core SDK.
import { useCharacteristic } from '@beacio/react';
const {
value, // Latest DataView value
isNotifying, // Currently subscribed?
read, // () => Promise<DataView | null>
write, // (value: BufferSource) => Promise<void>
writeWithoutResponse,
subscribe, // (handler: (value: DataView) => void) => Promise<void>
unsubscribe,
error,
} = useCharacteristic(device, 'heart_rate', 'heart_rate_measurement');
// Read a value
const data = await read();
// Write a value
await write(new Uint8Array([0x01, 0x02]));
// Subscribe to notifications
await subscribe((value) => {
console.log('Heart rate:', value.getUint8(1));
});useNotifications(device, service, characteristic, options?)
Subscribe to characteristic notifications with a rolling history.
import { useNotifications } from '@beacio/react';
const {
value, // Latest DataView
history, // Array<{ timestamp: Date, value: DataView }>
isSubscribed,
subscribe, // () => Promise<void>
unsubscribe,
clear, // Clear history
error,
} = useNotifications(device, 'heart_rate', 'heart_rate_measurement', {
autoSubscribe: true,
maxHistory: 100,
});useScan()
Scan for nearby BLE devices.
import { useScan } from '@beacio/react';
const {
scanState, // 'idle' | 'scanning' | 'stopped'
devices, // BeacioDevice[]
start, // (options?: ScanOptions) => Promise<void>
stop,
clear,
error,
} = useScan();
await start({
filters: [{ namePrefix: 'Device' }],
keepRepeatedDevices: true,
});Components
<BeacioProvider>
Required context provider. Optionally accepts a pre-configured beacio instance.
import { BeacioProvider } from '@beacio/react';
// Auto-creates beacio instance
<BeacioProvider config={{ apiKey: 'wbl_xxxxx', operatorName: 'MyApp' }}>
<App />
</BeacioProvider>
// Or pass an existing instance (useful for testing)
<BeacioProvider ble={existingBleInstance}>
<App />
</BeacioProvider><DeviceScanner>
Device selection UI with scan controls.
import { DeviceScanner } from '@beacio/react';
<DeviceScanner
filters={[{ services: ['heart_rate'] }]}
onDeviceSelected={(device) => setDevice(device)}
autoConnect
maxDevices={10}
scanDuration={30000}
/><InstallationWizard>
Guides users through beacio extension installation on Safari iOS.
import { InstallationWizard } from '@beacio/react';
<InstallationWizard
onComplete={() => console.log('Extension installed!')}
/>Error Handling
All hooks return a BeacioError with .code and .suggestion fields:
const { error } = useDevice(device);
if (error) {
console.log(error.code); // e.g. 'GATT_OPERATION_FAILED'
console.log(error.suggestion); // e.g. 'Check that the device is in range'
}TypeScript
Types are re-exported from @beacio/core for convenience:
import type { BeacioDevice, BeacioError, RequestDeviceOptions } from '@beacio/react';
import type { ConnectionState, UseDeviceReturn } from '@beacio/react';Migrating from Vanilla Web Bluetooth
Wrap your app with <BeacioProvider> (see Quick Start), then replace direct
navigator.bluetooth calls with the hooks. The hooks own the connection and
subscription lifecycle and clean up automatically on unmount, so manual
gattserverdisconnected listeners and teardown code can be deleted.
Before (vanilla)
navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }]
})
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => {
characteristic.addEventListener('characteristicvaluechanged', (event) => {
console.log('Heart Rate:', event.target.value.getUint8(1));
});
return characteristic.startNotifications();
})
.catch(error => console.error('Error:', error));After (@beacio/react)
import { useBluetooth, useDevice, useNotifications } from '@beacio/react';
import type { BeacioDevice } from '@beacio/react';
function HeartRateMonitor() {
const { requestDevice } = useBluetooth();
const [device, setDevice] = useState<BeacioDevice | null>(null);
const { connect } = useDevice(device);
const { value, subscribe } = useNotifications(
device,
'heart_rate',
'heart_rate_measurement',
);
const handleConnect = async () => {
try {
const paired = await requestDevice({
filters: [{ services: ['heart_rate'] }]
});
if (paired) {
setDevice(paired);
await connect();
await subscribe();
}
} catch (error) {
console.error('Error:', error);
}
};
const heartRate = value ? value.getUint8(1) : 0;
return (
<div>
<button onClick={handleConnect}>Connect</button>
{heartRate > 0 && <div>Heart Rate: {heartRate} BPM</div>}
</div>
);
}Migration map
| Vanilla Web Bluetooth | @beacio/react |
|---|---|
| navigator.bluetooth.requestDevice(options) | useBluetooth().requestDevice(options) |
| navigator.bluetooth.getDevices() | useBluetooth().getDevices() |
| device.gatt.connect() / .disconnect() | useDevice(device).connect() / .disconnect() |
| gattserverdisconnected listener + manual retry | useDevice(device, { autoReconnect: true }) |
| server.getPrimaryServices() | useDevice(device).services |
| characteristic.readValue() | useCharacteristic(...).read() |
| characteristic.writeValue(data) | useCharacteristic(...).write(data) |
| startNotifications() + characteristicvaluechanged | useNotifications(...) or useCharacteristic(...).subscribe(handler) |
| navigator.bluetooth.requestLEScan(options) | useScan().start(options) |
Migration gotchas:
requestDevicemust still be called from a user gesture (button click) — the hook does not lift that platform requirement.- Hook state replaces module-level
device/servervariables; keep theBeacioDevicein React state and pass it to the other hooks. - Errors surface both from the thrown promise and the hook's
errorfield (aBeacioErrorwith.code/.suggestion). - Custom service classes wrapping raw characteristics usually collapse into a
small custom hook composed from
useCharacteristic/useNotifications.
Browser Support
| Browser | Support | Notes | |---------|---------|-------| | Safari iOS | Full | Requires beacio Extension | | Chrome 56+ | Full | Native Web Bluetooth | | Edge 79+ | Full | Native Web Bluetooth |
Publishing (maintainers)
Published to npm as @beacio/react
under the @beacio org (granular org read+write token required).
The tarball ships only dist/, README.md, CHANGELOG.md, AGENTS.md and
LICENSE (see files in package.json).
npm run build # tsup: ESM + CJS + type declarations
npm pack --dry-run # verify tarball contents
npm publish --access public # production release
npm publish --access public --tag beta # prereleaseThe former standalone
docs/MIGRATION.md,docs/API.mdandPUBLISHING.mdwere folded into this README (archived 2026-07-02 — see git history).
License
MIT
