> For the complete documentation index, see [llms.txt](https://docs.alignedlayer.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.alignedlayer.com/wallet-as-a-service/framework-guides/react.md).

# React

`@aligned-waas/sdk-react` mirrors the client's session state into React and wraps the common flows in hooks. All the network and crypto work stays in [`@aligned-waas/sdk`](/wallet-as-a-service/quickstart.md).

## Install

```sh
npm install @aligned-waas/sdk @aligned-waas/sdk-react
```

React 18 or newer.

## Set up the provider

```tsx
import { AlignedClient, localStorageAdapter, webPasskeyAdapter } from '@aligned-waas/sdk';
import { AlignedProvider } from '@aligned-waas/sdk-react';

// Once, outside the component tree. A client per render resets its caches
// and re-runs session restoration.
const aligned = new AlignedClient({
  publishableKey: 'aligned_pk_test_...',
  storage: localStorageAdapter(),
  passkey: webPasskeyAdapter(),
});

export function Root() {
  return (
    <AlignedProvider client={aligned}>
      <App />
    </AlignedProvider>
  );
}
```

## Sign in and send a transaction

```tsx
import { useAligned, useWallet } from '@aligned-waas/sdk-react';

function App() {
  const { ready, authenticated, user, startEmailOtp, verifyEmailOtp } = useAligned();
  const { wallet, create, sendTransaction } = useWallet();

  if (!ready) return null; // session restoration still in flight
  if (!authenticated) return <SignIn onStart={startEmailOtp} onVerify={verifyEmailOtp} />;
  if (!wallet) {
    return (
      <button onClick={() => create({ userId: user!.id, userName: user!.email })}>
        Create wallet
      </button>
    );
  }

  return (
    <button onClick={() => sendTransaction({ to: '0x...', value: 1_000_000_000n })}>
      Send
    </button>
  );
}
```

`ready` is false until the SDK has finished trying to restore a session from storage. Render nothing auth-dependent before then, or returning users see a flash of the signed-out UI.

Gate passkey registration on `user` being non-null rather than on `authenticated`: a restored session whose identity lookup failed is authenticated but has no `user`, and `user.id` is what the passkey is registered against.

## Hooks

| Hook                | What you get                                                                                                                                                                                         |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useAligned()`      | `ready`, `authenticated`, `user`, `wallet`, plus `startEmailOtp`, `verifyEmailOtp`, `loginWithGooglePopup`, `logout`, `deleteAccount`, and the underlying `client`.                                  |
| `useWallet()`       | `wallet` plus `create`, `sendTransaction`, `signMessage`, `refresh`. Updates the wallet for every consumer of the provider.                                                                          |
| `useKeys()`         | `active`, `pending`, `timelocked`, `activeOnThisDevice`, `deviceCredentials`, with `requestWithPasskey`, `approveWithPasskey`, `removeWithPasskey`, `cancelRecovery`, `rename`, `reject`, `refresh`. |
| `useActiveKey()`    | Which key this device signs with: `activeKeyHash`, `selectKey`, `reselectSigningKey`, `adoptExisting`.                                                                                               |
| `useDeviceAccess()` | Runs the device-access check (see [Keys and recovery](/wallet-as-a-service/keys-and-recovery.md)): `status`, `needsDevice`, `recovery`, `check`.                                                     |
| `usePasskey()`      | The raw passkey adapter, for custom ceremonies.                                                                                                                                                      |

Anything not covered is on `useAligned().client`.

## Two things that surprise people

`signMessage('hi')` hashes with `keccak256(utf8(message))`, not EIP-191 personal\_sign as in ethers or viem. Pass `{ hash }` if you need EIP-191 or EIP-712 semantics.

`create()` returns the existing wallet with `created: false` when the user already has one, and registers no passkey in that case. Adding a device is `useKeys()`, not `create()`.
