# Quickstart — React

Wire `@ios-web-bluetooth/core` into a React app with a typed hook from `@ios-web-bluetooth/react`.

## Prerequisites

- The WebBLE Safari Web Extension enabled on your origin. See the [base quickstart](quickstart.md).
- React 18+ (hooks). Works with Vite, CRA, Next, Remix, Astro islands.

## 1. Install

```bash
npm install @ios-web-bluetooth/core @ios-web-bluetooth/react
```

## 2. Mount the polyfill

Import the core package once at the entry point so `navigator.bluetooth` is installed before any component renders.

```tsx
// main.tsx
import '@ios-web-bluetooth/core/auto';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')!).render(<App />);
```

## 3. Read a heart-rate monitor

```tsx
// HeartRate.tsx
import { useEffect, useState } from 'react';
import { useConnection } from '@ios-web-bluetooth/react';

export function HeartRate() {
  const { device, status, isConnected, connect, disconnect, error } = useConnection({
    filters: [{ services: ['heart_rate'] }]
  });
  const [bpm, setBpm] = useState<number | null>(null);

  useEffect(() => {
    if (!isConnected || !device?.gatt) return;
    let char: BluetoothRemoteGATTCharacteristic | undefined;
    const onChange = (ev: Event) => {
      const v = (ev.target as BluetoothRemoteGATTCharacteristic).value!;
      setBpm(v.getUint8(0) & 0x01 ? v.getUint16(1, true) : v.getUint8(1));
    };
    (async () => {
      const server = await device.gatt!.connect();
      const service = await server.getPrimaryService('heart_rate');
      char = await service.getCharacteristic('heart_rate_measurement');
      await char.startNotifications();
      char.addEventListener('characteristicvaluechanged', onChange);
    })();
    return () => char?.removeEventListener('characteristicvaluechanged', onChange);
  }, [isConnected, device]);

  if (error) return <p>Error: {error.message}</p>;
  if (!isConnected) {
    return (
      <button onClick={connect} disabled={status !== 'idle'}>
        {status === 'idle' ? 'Connect' : status}
      </button>
    );
  }
  return (
    <>
      <p>{bpm ?? '—'} bpm</p>
      <button onClick={disconnect}>Disconnect</button>
    </>
  );
}
```

`useConnection` is the all-in-one hook for a single-device session: it drives the click-triggered `requestDevice()` picker and GATT connect from one `connect()` call and exposes `{ device, status, isConnected, connect, disconnect, error }`. `status` moves through `idle → requesting → connecting → connected` (and `disconnected`), so disable the trigger while it is not `idle`.

## 4. Feature-detect iOS premium surface

```tsx
import { useEffect, useState } from 'react';

export function usePremium() {
  const [premium, setPremium] = useState(false);
  useEffect(() => {
    setPremium(typeof window !== 'undefined' && 'webbleIOS' in window);
  }, []);
  return premium;
}
```

If `webbleIOS` is present the page can use [premium APIs](premium.md); if not, fall back to the standard surface only.

## Next

- [API reference](api-reference.md)
- [Recipes](recipes.md)
- [Premium APIs](premium.md)
