---
title: "React"
description: "How to integrate the Wavelength SDK into a React application using the provider-injected engine, the available hooks, and the passkey ceremony helpers."
canonical: https://wavelength.lightning.engineering/integrations/react/
---

> Docs index: https://wavelength.lightning.engineering/llms.txt

# React

## Provider (injected engine)

`@lightninglabs/wavelength-react` is **transport-agnostic**: it depends only on `@lightninglabs/wavelength-core` and never imports a specific transport. The provider takes a prebuilt **`WalletEngine`**; each transport ships its own factory to build one (`createWebWalletEngine` from [`@lightninglabs/wavelength-web`](/reference/wavelength-web/) for the web transport, `createNativeWalletEngine` from [`@lightninglabs/wavelength-react-native`](/reference/wavelength-react-native/) for React Native).

Build the engine once, typically at module scope outside any component, and pass it to `WavelengthProvider`. Set `autoStart: true` (with `config`) to boot the runtime as soon as it is ready, instead of wiring a boot `useEffect` yourself:

```tsx
import { createWebWalletEngine, defaultConfig } from '@lightninglabs/wavelength-web';
import { WavelengthProvider } from '@lightninglabs/wavelength-react';

const engine = createWebWalletEngine({
  runtimeBaseUrl: 'https://your-host/wavewalletdk/',
  config: defaultConfig('signet'),
  autoStart: true,
});

export function App() {
  return <WavelengthProvider engine={engine}>{/* your app */}</WavelengthProvider>;
}
```

```tsx
import { createNativeWalletEngine, defaultConfig } from '@lightninglabs/wavelength-react-native';
import { WavelengthProvider } from '@lightninglabs/wavelength-react';

const engine = createNativeWalletEngine({
  config: defaultConfig('signet'),
  autoStart: true,
});

export default function App() {
  return (
    <WavelengthProvider engine={engine}>
      {/* your app */}
    </WavelengthProvider>
  );
}
```

The engine is built once outside the component tree, so `WavelengthProvider` owns nothing beyond publishing it to `useSyncExternalStore` subscribers: it never disposes the engine on unmount. Every lifecycle behavior (the sync-poll while `phase === 'syncing'`, activity-driven balance refresh, the restore readiness poll, and an unsolicited `'stopped'` transition on a runtime crash) is owned by the `WalletEngine` itself and works the same whether or not React is in the picture. If you would rather not use `autoStart`, call `start()` from [`useWallet()`](/reference/wavelength-react/#useWallet) yourself once `phase === 'runtimeReady'`.

`phase` is a `RuntimePhase`, one of: `'loading'`, `'runtimeReady'`, `'starting'`, `'needsWallet'`, `'locked'`, `'syncing'`, `'restoring'`, `'ready'`, `'stopping'`, `'stopped'`, or `'error'`. The `loading`/ `runtimeReady`/`starting`/`stopping`/`stopped`/`error` phases are owned by the start/stop flow; `needsWallet`/`locked`/`syncing`/`ready` are derived from wallet info; `restoring` is owned by the engine while a background restore (recovery scan) is bringing a freshly restored wallet up. See [Handle phases and errors](/guides/handle-phases-and-errors/) for the full state machine and how to react to each phase.

The package **re-exports every type and enum from `core`**, including `WalletEngine` and `createWalletEngine`, so a React app can import contracts from one place. It does **not** re-export any transport’s engine factory; keep the transport import separate so the same binding runs over any transport, web or React Native.

## Hooks

**`useWallet()`** is the application-shell hook: `phase`, `error`, `start`, `stop`. It throws outside a provider. Read the rest of the state through the granular hooks below.

**State-reading hooks** are plain selectors; each re-renders only when the slice it reads changes:

| Hook                  | Returns                     |
| --------------------- | --------------------------- |
| `useWalletInfo()`     | `WalletInfo \| null`        |
| `useWalletBalance()`  | `Balance \| null`           |
| `useWalletActivity()` | `Entry[]`                   |
| `useWalletRecovery()` | `{ recovery, acknowledge }` |
| `useWalletLogs()`     | `{ logs, clear }`           |

**Mutation hooks** each expose an action plus verb-prefixed `{ <verb>Pending, <verb>Error, <verb>Data, reset<Verb> }` fields built on one shared convention, throw-and-capture: the action both settles its own promise (resolve with the result, or reject with the failure) and mirrors that outcome into the pending/error/data fields, so you can `await`/`catch` it imperatively or read the hook’s fields declaratively, whichever fits. Error fields are always an `Error | null`, never a string:

| Hook                     | Returns                                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
| `useWalletCreate()`      | `{ create, createPending, createError, createData, resetCreate }`                                      |
| `useWalletRestore()`     | `{ restore, restorePending, restoreError, restoreData, resetRestore }`                                 |
| `useWalletUnlock()`      | `{ unlock, unlockPending, unlockError, unlockData, resetUnlock }`                                      |
| `useWalletDeposit()`     | `{ deposit, depositPending, depositError, depositData, resetDeposit }`                                 |
| `useWalletReceive()`     | `{ receive, receivePending, receiveError, receiveData, resetReceive }`                                 |
| `useWalletPrepareSend()` | `{ prepare, preparePending, prepareError, prepareData, resetPrepare }`                                 |
| `useWalletSend()`        | `{ send, sendPrepared, sendPending, sendError, sendData, resetSend }`                                  |
| `useWalletRefresh()`     | `{ refresh, refreshPending, refreshError, resetRefresh }` (no data field: `refresh()` resolves `void`) |

**Exit hooks** cover cooperative and unilateral exits. `useWalletExit`, `useWalletExitPlan`, `useWalletSweep`, and `useWalletList` are 1:1 wrappers around one daemon verb and follow the same throw-and-capture convention as the mutation hooks above. `useWalletExitBatch` is the orchestrator: it drives a multi-outpoint exit through funding-contention guards and additionally reports progress events. `useWalletExitStatus` and `useWalletExits` are read hooks instead of mutations: they fetch on mount (and on relevant changes) and expose a `refresh*` action rather than a `reset*` one:

| Hook                                   | Returns                                                                                           |
| -------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `useWalletExit()`                      | `{ exit, exitPending, exitError, exitData, resetExit }`                                           |
| `useWalletExitPlan()`                  | `{ plan, planPending, planError, planData, resetPlan }`                                           |
| `useWalletSweep()`                     | `{ sweep, sweepPending, sweepError, sweepData, resetSweep }`                                      |
| `useWalletExitBatch()`                 | `{ exitBatch, exitBatchPending, exitBatchError, exitBatchData, exitBatchEvents, resetExitBatch }` |
| `useWalletExitStatus(outpoint, opts?)` | `{ status, statusPending, statusError, refreshStatus }`                                           |
| `useWalletExits()`                     | `{ summary, summaryPending, summaryError, refreshSummary }`                                       |
| `useWalletList()`                      | `{ list, listPending, listError, listData, resetList }`                                           |

`useWalletExitStatus` defaults to a cheap, coarse phase-only fetch; pass `{ detailed: true }` for recovery-tree progress, the CSV countdown, and fees. Pass `{ pollMs }` to poll while the hook is mounted: polling is opt-in and runs with no visibility/focus gating, stopping only on unmount. See [Unilateral exit & status](/guides/unilateral-exit/) for end-to-end usage, and the [wavelength-react reference](/reference/wavelength-react/#exiting-ark) for the full hazard notes on `exitBatch`.

For example, `useWalletSend()` returns a `send(req)` action alongside its own `sendPending`/`sendError` state:

```tsx
import { useWalletSend } from '@lightninglabs/wavelength-react';

function SendButton({ invoice }: { invoice: string }) {
  const { send, sendPending, sendError } = useWalletSend();

  return (
    <>
      <button disabled={sendPending} onClick={() => send({ invoice })}>
        Send payment
      </button>
      {sendError && <p role="alert">{sendError.message}</p>}
    </>
  );
}
```

See [Send a payment](/guides/send-a-payment/), [Receive a Lightning payment](/guides/receive-a-lightning-payment/), [Get a deposit address](/guides/get-a-deposit-address/), and [Show balance and activity](/guides/show-balance-and-activity/) for end-to-end usage of these hooks.

`useWalletCreate()` and `useWalletRestore()` are two separate hooks (mirroring `WalletEngine.createWallet` and `WalletEngine.restoreWallet`) rather than one combined bootstrap hook, so a component only subscribes to the flow it drives:

```tsx
import { useWalletCreate } from '@lightninglabs/wavelength-react';

function Onboard() {
  const { create, createPending, createError } = useWalletCreate();

  const handleCreate = async () => {
    const { mnemonic } = await create({ password: 'a-strong-password' });
    // Back up `mnemonic` securely before the user sends funds.
    console.log('Wallet created');
  };

  return (
    <>
      <button onClick={handleCreate} disabled={createPending}>
        Create wallet
      </button>
      {createError && <p role="alert">{createError.message}</p>}
    </>
  );
}
```

Once `create` resolves, `phase` from `useWallet()` automatically advances (through `'syncing'` when the daemon needs to catch up) to `'ready'`, so any component watching `phase` updates without extra wiring.

## Passkey

Passkeys are driven by **`useWalletPasskey(ceremony)`**, where `ceremony` is a `PasskeyCeremony` implementation injected from your transport. On web, pass **`webPasskeyCeremony`** from `@lightninglabs/wavelength-web`; on React Native, pass **`createNativePasskeyCeremony({ rpId })`** from `@lightninglabs/wavelength-react-native` (see [Passkey setup](/react-native/get-started/passkey-setup/) for the relying-party domain association it requires). `create` and `open` track separately, each with its own `pending`/`error`/`reset`:

```tsx
import { useWalletPasskey } from '@lightninglabs/wavelength-react';
import { webPasskeyCeremony } from '@lightninglabs/wavelength-web';

function PasskeyUnlock() {
  const {
    supported,
    create,
    createPending,
    createError,
    open,
    openPending,
    openError,
  } = useWalletPasskey(webPasskeyCeremony);

  if (!supported) {
    return <p>Passkeys are not available on this device.</p>;
  }

  return (
    <>
      <button disabled={createPending} onClick={() => create('My App')}>
        Create with passkey
      </button>
      <button disabled={openPending} onClick={() => open(/* optional credential id */)}>
        Unlock with passkey
      </button>
      {createError && <p role="alert">{createError.message}</p>}
      {openError && <p role="alert">{openError.message}</p>}
    </>
  );
}
```

```tsx
import { Button, Text } from 'react-native';
import { useWalletPasskey } from '@lightninglabs/wavelength-react';
import { createNativePasskeyCeremony } from '@lightninglabs/wavelength-react-native';

const ceremony = createNativePasskeyCeremony({ rpId: 'your-app-domain.example' });

function PasskeyUnlock() {
  const {
    supported,
    create,
    createPending,
    createError,
    open,
    openPending,
    openError,
  } = useWalletPasskey(ceremony);

  if (!supported) {
    return <Text>Passkeys are not available on this device.</Text>;
  }

  return (
    <>
      <Button
        disabled={createPending}
        onPress={() => create('My App')}
        title="Create with passkey"
      />
      <Button
        disabled={openPending}
        onPress={() => open(/* optional credential id */)}
        title="Unlock with passkey"
      />
      {createError && <Text role="alert">{createError.message}</Text>}
      {openError && <Text role="alert">{openError.message}</Text>}
    </>
  );
}
```

The hook checks **`ceremony.supportsPasskeyPrf()`** on mount (platform authenticator availability). **`create(appName)`** runs `registerPasskeyWallet`, then opens the wallet from the derived PRF output, then refreshes engine state. **`open(credentialId?)`** asserts an existing passkey (scoped when an id is passed, discoverable otherwise) and opens the wallet the same way. On success the engine’s phase advances automatically; persist the returned **`credentialId`** if you want scoped unlocks later.

Unlike the rest of the hooks on this page, `create`/`open` are the one place where a failure can be something other than a real error: dismissing the OS passkey prompt rejects with a `PasskeyCancelledError`, which `useWalletPasskey` rethrows without recording into `createError`/`openError` (a cancelled prompt is not a failure worth displaying). Every other rejection behaves like the mutation hooks above: `await`/`catch` the call, or read `createError`/ `openError`.

Both `create` and `open` resolve to a `PasskeyWalletOutcome`: `{ result, credentialId }`. Capture `credentialId` from a successful result and store it so a later `open(credentialId)` call can perform a scoped unlock instead of a discoverable one:

```tsx
const outcome = await create('My App');
localStorage.setItem('passkeyCredentialId', outcome.credentialId);
```

See [Use a passkey](/guides/use-a-passkey/) for end-to-end onboarding UX.
