npm package: @solana/react-hooks
Description: React hooks for the @solana/client Solana client
Author: Solana Maintainers
Latest version: 1.4.1

# @solana/react-hooks

React hooks for `@solana/client`. Wrap your app once and reach for hooks instead of wiring RPC, wallets, and stores by hand.

## Install

```bash
npm install @solana/client @solana/react-hooks
```

## Quickstart

1. Choose wallet connectors (auto-discovery is the fastest way to start).
2. Create a Solana client.
3. Wrap your tree with `SolanaProvider` and use the hooks.

```tsx
import { autoDiscover, createClient } from "@solana/client";
import {
  SolanaProvider,
  useBalance,
  useWalletConnection,
} from "@solana/react-hooks";

const client = createClient({
  endpoint: "https://api.devnet.solana.com",
  walletConnectors: autoDiscover(),
});

export function App() {
  return (
    <SolanaProvider client={client}>
      {/* your components that call hooks go here */}
    </SolanaProvider>
  );
}
```

> **Next.js / RSC:** Components that call these hooks must be marked with `'use client'`.

## Common Solana flows (copy/paste)

These snippets assume a parent already handled wallet connection and can pass an address where needed.

### Connect, disconnect, and show balance

```tsx
function WalletPanel() {
  const { connectors, connect, disconnect, wallet, status, currentConnector } =
    useWalletConnection();
  const address = wallet?.account.address;
  const balance = useBalance(address);

  if (status === "connected") {
    return (
      <div>
        <p>Connected via {currentConnector?.name}</p>
        <p>{address?.toString()}</p>
        <p>Lamports: {balance.lamports?.toString() ?? "loading…"}</p>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    );
  }

  return connectors.map((c) => (
    <button key={c.id} onClick={() => connect(c.id)}>
      Connect {c.name}
    </button>
  ));
}
```

### Read lamport balance (auto fetch + watch)

```tsx
import { useBalance } from "@solana/react-hooks";

function BalanceCard({ address }: { address: string }) {
  const { lamports, fetching, slot } = useBalance(address);
  if (fetching) return <p>Loading…</p>;
  return (
    <p>
      Lamports: {lamports?.toString() ?? "0"} (slot {slot?.toString() ?? "—"})
    </p>
  );
}
```

### Read account data (auto fetch + watch)

```tsx
import { useAccount } from "@solana/react-hooks";

function AccountInfo({ address }: { address: string }) {
  const account = useAccount(address);
  if (!account || account.fetching) return <p>Loading…</p>;
  return (
    <div>
      <p>Lamports: {account.lamports?.toString() ?? "0"}</p>
      <p>Owner: {account.owner ?? "—"}</p>
      <p>Slot: {account.slot?.toString() ?? "—"}</p>
    </div>
  );
}
```

### Send SOL

```tsx
import { useSolTransfer } from "@solana/react-hooks";

function SendSol({ destination }: { destination: string }) {
  const { send, isSending, status, signature, error } = useSolTransfer(); // expects a connected wallet
  return (
    <div>
      <button
        disabled={isSending}
        onClick={() =>
          send({ destination, amount: 100_000_000n /* 0.1 SOL */ })
        }
      >
        {isSending ? "Sending…" : "Send 0.1 SOL"}
      </button>
      <p>Status: {status}</p>
      {signature ? <p>Signature: {signature}</p> : null}
      {error ? <p role="alert">Error: {String(error)}</p> : null}
    </div>
  );
}
```

### SPL token balance + transfer

```tsx
import { useSplToken } from "@solana/react-hooks";

function TokenPanel({
  mint,
  destinationOwner,
}: {
  mint: string;
  destinationOwner: string;
}) {
  const {
    balance,
    send,
    isSending,
    owner,
    status,
    error,
    sendError,
    sendSignature,
    resetSend,
  } = useSplToken(mint);

  if (status === "disconnected") return <p>Connect wallet to view balance</p>;
  if (status === "loading") return <p>Loading balance…</p>;
  if (status === "error") return <p role="alert">Error: {String(error)}</p>;

  return (
    <div>
      <p>Owner: {owner}</p>
      <p>Balance: {balance?.uiAmount ?? "0"}</p>
      <button
        disabled={isSending || !owner}
        onClick={() => send({ amount: 1n, destinationOwner, amountInBaseUnits: true })}
      >
        {isSending ? "Sending…" : "Send 1 token"}
      </button>
      {sendSignature ? <p>Signature: {sendSignature}</p> : null}
      {sendError ? (
        <div>
          <p role="alert">Send failed: {String(sendError)}</p>
          <button onClick={resetSend}>Dismiss</button>
        </div>
      ) : null}
    </div>
  );
}
```

> **Note:** Use `amountInBaseUnits: true` when passing raw bigint amounts. For human-readable decimal strings like `"1.5"`, omit the flag.

**Available properties:**
- `balance` / `owner` — token balance and owner address
- `status` — overall hook status ('disconnected' | 'error' | 'loading' | 'ready')
- `error` — error from balance fetching
- `send(config, opts?)` — transfer tokens to destination
- `isSending` / `sendStatus` / `sendError` / `sendSignature` — transfer state
- `refresh()` / `refreshing` — manually refresh balance
- `resetSend()` — clear send error state
- `helper` — low-level helper for advanced use

**Options (second parameter):**
- `commitment` — RPC commitment level
- `owner` — override balance owner (defaults to connected wallet)
- `revalidateOnFocus` — refresh when window regains focus
- `swr` — additional SWR options
- `config.tokenProgram` — token program: `'auto'` for detection, or explicit address

### Token 2022 support

Token 2022 mints are supported via the `tokenProgram` config option:

```tsx
// Auto-detect Token or Token 2022
const { balance, send } = useSplToken(mint, {
  config: { tokenProgram: "auto" },
});

// Balance and transfers work the same way
<p>Balance: {balance?.uiAmount ?? "0"}</p>
```

### Fetch address lookup tables

```tsx
import { useLookupTable } from "@solana/react-hooks";

function LookupTableInfo({ address }: { address: string }) {
  const { data, isLoading, error } = useLookupTable(address);
  if (isLoading) return <p>Loading…</p>;
  if (error) return <p role="alert">Error loading LUT</p>;
  return (
    <div>
      <p>Addresses in LUT: {data?.addresses.length ?? 0}</p>
      <p>Authority: {data?.authority ?? "None"}</p>
    </div>
  );
}
```

### Fetch nonce accounts

```tsx
import { useNonceAccount } from "@solana/react-hooks";

function NonceInfo({ address }: { address: string }) {
  const { data, isLoading, error } = useNonceAccount(address);
  if (isLoading) return <p>Loading…</p>;
  if (error) return <p role="alert">Error loading nonce</p>;
  return (
    <div>
      <p>Nonce: {data?.blockhash}</p>
      <p>Authority: {data?.authority}</p>
    </div>
  );
}
```

### Build and send arbitrary transactions

```tsx
import type { TransactionInstructionInput } from "@solana/client";
import { useTransactionPool, useWalletSession } from "@solana/react-hooks";

function TransactionFlow({ ix }: { ix: TransactionInstructionInput }) {
  const session = useWalletSession();
  const {
    addInstruction,
    prepareAndSend,
    isSending,
    sendSignature,
    sendError,
    latestBlockhash,
  } = useTransactionPool();

  return (
    <div>
      <button onClick={() => addInstruction(ix)}>Add instruction</button>
      <button
        disabled={isSending || !session}
        onClick={() => prepareAndSend({ authority: session })}
      >
        {isSending ? "Sending…" : "Prepare & Send"}
      </button>
      <p>Blockhash: {latestBlockhash.blockhash ?? "loading…"}</p>
      {sendSignature ? <p>Signature: {sendSignature}</p> : null}
      {sendError ? <p role="alert">{String(sendError)}</p> : null}
    </div>
  );
}
```

**Available properties:**
- `addInstruction(ix)` / `addInstructions(ixs)` — queue instructions
- `removeInstruction(index)` / `clearInstructions()` / `replaceInstructions(ixs)` — manage queue
- `instructions` — current instruction queue
- `prepare(opts)` / `prepareAndSend(opts)` — build and optionally send
- `send(opts)` / `sign(opts)` — send or sign prepared transaction
- `isPreparing` / `prepareStatus` / `prepareError` — prepare state
- `isSending` / `sendStatus` / `sendError` / `sendSignature` — send state
- `latestBlockhash` — current blockhash for lifetime
- `prepared` / `toWire()` — access prepared transaction
- `reset()` — clear all state

### Simple mutation helper (when you already have instructions)

```tsx
import { useSendTransaction } from "@solana/react-hooks";

function SendPrepared({ instructions }) {
  const { send, isSending, status, signature, error } = useSendTransaction();
  return (
    <div>
      <button disabled={isSending} onClick={() => send({ instructions })}>
        {isSending ? "Submitting…" : "Send transaction"}
      </button>
      <p>Status: {status}</p>
      {signature ? <p>Signature: {signature}</p> : null}
      {error ? <p role="alert">{String(error)}</p> : null}
    </div>
  );
}
```

> **Note:** This hook automatically uses the connected wallet session — no need to pass `authority` explicitly.

**Available properties:**
- `send(request, opts?)` — build and send transaction
- `sendPrepared(prepared, opts?)` — send already-prepared transaction
- `isSending` / `status` / `signature` / `error` — transaction state
- `reset()` — clear state for new transaction

### Track confirmations for a signature

```tsx
import { useWaitForSignature } from "@solana/react-hooks";

function SignatureWatcher({ signature }: { signature: string }) {
  const wait = useWaitForSignature(signature, { commitment: "finalized" });
  if (wait.waitStatus === "error") return <p role="alert">Failed</p>;
  if (wait.waitStatus === "success") return <p>Finalized ✅</p>;
  if (wait.waitStatus === "waiting") return <p>Waiting…</p>;
  return <p>Provide a signature</p>;
}
```

### Query program accounts

```tsx
import { SolanaQueryProvider, useProgramAccounts } from "@solana/react-hooks";

function ProgramAccounts({ program }: { program: string }) {
  const query = useProgramAccounts(program);
  if (query.isLoading) return <p>Loading…</p>;
  if (query.isError) return <p role="alert">RPC error</p>;
  return (
    <div>
      <button onClick={() => query.refresh()}>Refresh</button>
      <ul>
        {query.accounts.map(({ pubkey }) => (
          <li key={pubkey.toString()}>{pubkey.toString()}</li>
        ))}
      </ul>
    </div>
  );
}

function ProgramAccountsSection({ program }: { program: string }) {
  return (
    <SolanaQueryProvider>
      <ProgramAccounts program={program} />
    </SolanaQueryProvider>
  );
}
```

### Simulate a transaction

```tsx
import { useSimulateTransaction } from "@solana/react-hooks";

function Simulation({ wire }: { wire: string }) {
  const sim = useSimulateTransaction(wire);
  if (sim.isLoading) return <p>Simulating…</p>;
  if (sim.isError) return <p role="alert">Simulation failed</p>;
  return (
    <div>
      <button onClick={() => sim.refresh()}>Re-run</button>
      <pre>{JSON.stringify(sim.logs, null, 2)}</pre>
    </div>
  );
}
```

## Using Suspense (opt-in)

Enable Suspense per subtree by setting `suspense` on `SolanaQueryProvider` and wrapping content in a React `<Suspense>` boundary. This keeps the rest of the UI non-blocking.

```tsx
import { SolanaQueryProvider, useBalance } from "@solana/react-hooks";
import { Suspense } from "react";

function BalanceDetails({ address }: { address: string }) {
  const balance = useBalance(address);
  return <p>Lamports: {balance.lamports?.toString() ?? "0"}</p>;
}

export function WalletPanel({ address }: { address: string }) {
  return (
    <SolanaQueryProvider suspense>
      <Suspense fallback={<p>Loading balance…</p>}>
        <BalanceDetails address={address} />
      </Suspense>
    </SolanaQueryProvider>
  );
}
```

## Provider SWR config (optional)

```tsx
export function App() {
  return (
    <SolanaProvider
      client={client}
      query={{
        config: {
          revalidateOnFocus: false,
          revalidateOnReconnect: false,
          refreshInterval: 30_000,
        },
      }}
    >
      <WalletPanel />
    </SolanaProvider>
  );
}
```

Defaults when you omit `query.config`:
- `revalidateOnFocus` / `revalidateOnReconnect` / `revalidateIfStale`: `true`
- `dedupingInterval`: `2000ms`
- `focusThrottleInterval`: `5000ms`

SWR background: stale-while-revalidate (RFC 5861): https://datatracker.ietf.org/doc/html/rfc5861

### Work with the client store directly

```tsx
import { useClientStore } from "@solana/react-hooks";

function ClusterBadge() {
  const cluster = useClientStore((s) => s.cluster);
  return <p>Endpoint: {cluster.endpoint}</p>;
}
```

## Wallet connector filtering

Use `filterByNames` with `autoDiscover()` to filter wallets by name without wallet-specific code:

```tsx
import { autoDiscover, createClient, filterByNames } from "@solana/client";

// Only show Phantom and Solflare
const client = createClient({
  cluster: "devnet",
  walletConnectors: autoDiscover({
    filter: filterByNames("phantom", "solflare"),
  }),
});
```

This approach follows Wallet Standard's wallet-agnostic discovery pattern while still allowing you to curate which wallets appear in your app.

## Notes and defaults

- Wallet connectors: use `autoDiscover()` to pick up Wallet Standard injectables; use `filterByNames()` to filter by name, or explicitly compose `phantom()`, `solflare()`, `backpack()`, `metamask()`, etc.
- Queries: all RPC query hooks accept `swr` options under `swr` and `disabled` flags. Suspense is opt-in via `SolanaQueryProvider`’s `suspense` prop.
- Authorities: transaction helpers default to the connected wallet session when `authority` is omitted.
- Types: every hook exports `UseHookNameParameters` / `UseHookNameReturnType` aliases.

## More resources

- [Documentation](https://www.framework-kit.com/) — full guides and API reference
- Playground: `examples/vite-react` (run with `pnpm install && pnpm dev`).
- Next.js reference app: `examples/nextjs`.
- Hook JSDoc lives in `src/hooks.ts`, `src/queryHooks.ts`, `src/ui.tsx`.