228 lines
12 KiB
Markdown
228 lines
12 KiB
Markdown
---
|
|
title: React Reference
|
|
description: Kit-native React bindings from @solana/react (ClientProvider, typed useClient, data hooks, SWR/TanStack adapters) and wallet React hooks from @solana/kit-plugin-wallet/react.
|
|
---
|
|
|
|
# Solana Kit React Reference
|
|
|
|
Two packages cover React apps:
|
|
|
|
1. **`@solana/react` (v7+)** — Kit client bindings: `ClientProvider`, `useClient`, `useClientCapability`, data hooks (`useAction`, `useRequest`, `useSubscription`, `useTrackedData`), and adapters for SWR (`@solana/react/swr`) and TanStack Query (`@solana/react/query`).
|
|
2. **`@solana/kit-plugin-wallet/react`** — wallet connection hooks (see below).
|
|
|
|
> **Deprecation note:** the older Wallet Standard hooks that shipped in `@solana/react` (`SelectedWalletAccountContextProvider`, `useSelectedWalletAccount`, `useSignIn` / `useSignMessage` / `useSignTransaction` / `useSignAndSendTransaction`, `useWalletAccount*Signer`) are being superseded by the wallet-plugin hooks and will be deprecated. Do not use them in new code.
|
|
|
|
## Client Provider + Typed useClient
|
|
|
|
Create one client for the app, export its type, and provide it at the root:
|
|
|
|
```tsx
|
|
// app/providers.tsx
|
|
import { createClient } from '@solana/kit';
|
|
import { solanaRpc } from '@solana/kit-plugin-rpc';
|
|
import { walletSigner } from '@solana/kit-plugin-wallet';
|
|
import { ClientProvider } from '@solana/react';
|
|
|
|
export const client = createClient()
|
|
.use(walletSigner({ chain: 'solana:devnet' }))
|
|
.use(solanaRpc({ rpcUrl }));
|
|
|
|
// Makes every useClient<AppClient>() call fully typed
|
|
export type AppClient = Awaited<typeof client>;
|
|
|
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
return <ClientProvider client={client}>{children}</ClientProvider>;
|
|
}
|
|
```
|
|
|
|
Always pass your client type to `useClient` — as of `@solana/react` 7.1+, the `TClient` type parameter is required, so a bare `useClient()` fails to compile:
|
|
|
|
```tsx
|
|
import { useClient } from '@solana/react';
|
|
import type { AppClient } from '@/app/providers';
|
|
|
|
function Balance({ address }: { address: Address }) {
|
|
const client = useClient<AppClient>();
|
|
// client.rpc, client.wallet, client.sendTransaction — all typed
|
|
// data hooks: useRequest / useSubscription / useTrackedData / useAction
|
|
// or use the SWR / TanStack Query adapters for caching + revalidation
|
|
}
|
|
```
|
|
|
|
## Data Hooks
|
|
|
|
| Hook | Purpose |
|
|
|------|---------|
|
|
| `useRequest` | One-shot async reads (RPC calls) |
|
|
| `useSubscription` | WebSocket subscriptions with cleanup |
|
|
| `useTrackedData` | One-shot read seeded into a subscription, slot-deduped |
|
|
| `useAction` | Wrap async actions (send, connect) with pending/error state |
|
|
|
|
For caching, revalidation, and request dedup, prefer the framework adapters: `@solana/react/swr` (`useRequestSWR`, `useSubscriptionSWR`, `useTrackedDataSWR`) and `@solana/react/query` (`useRequestQuery`, `useSubscriptionQuery`, `useTrackedDataQuery`). Both are optional peer deps — install `swr` or `@tanstack/react-query` yourself.
|
|
|
|
As of `@solana/react` 7.1+, `useSubscriptionQuery` / `useTrackedDataQuery` (the TanStack Query adapters) surface a new error, `SOLANA_ERROR__SUBSCRIBABLE__STREAM_CLOSED_WITHOUT_ERROR`, when the underlying stream store closes in an error state with a nullish payload. The SWR adapters are unaffected.
|
|
|
|
### useTrackedData / useTrackedDataSWR / useTrackedDataQuery
|
|
|
|
Use these for any live account value (balances, token accounts, program state). The hook fires the initial RPC read and the subscription together and slot-dedupes them, so the first paint is fast and out-of-order arrivals never regress the surfaced value. Do **not** hand-roll a `getBalance` + `accountNotifications` pair.
|
|
|
|
```tsx
|
|
import { useMemo } from 'react';
|
|
import { type Address, type Lamports } from '@solana/kit';
|
|
import { useClient } from '@solana/react';
|
|
import { useTrackedDataSWR } from '@solana/react/swr';
|
|
|
|
function useBalance(accountAddress: Address) {
|
|
const { rpc, rpcSubscriptions } = useClient<AppClient>();
|
|
const spec = useMemo(
|
|
() => ({
|
|
initialValueSource: rpc.getBalance(accountAddress, { commitment: 'confirmed' }),
|
|
initialValueMapper: (lamports: Lamports) => lamports,
|
|
streamSource: rpcSubscriptions.accountNotifications(accountAddress, {
|
|
commitment: 'confirmed',
|
|
}),
|
|
streamValueMapper: ({ lamports }: { lamports: Lamports }) => lamports,
|
|
}),
|
|
[rpc, rpcSubscriptions, accountAddress],
|
|
);
|
|
const { data, error } = useTrackedDataSWR(['balance', accountAddress], spec);
|
|
return { lamports: data?.value ?? null, error };
|
|
}
|
|
```
|
|
|
|
- `data` is the `SolanaRpcResponse` envelope: `data.value` and `data.context.slot`.
|
|
- The `spec` must be memoized — identity drives teardown/re-run. Pass `null` (for the spec, or the SWR key) to disable.
|
|
- `useTrackedDataSWR` returns SWR's `{ data, error }` only. If you need a `refresh()` button or per-attempt `getAbortSignal` timeouts, use plain `useTrackedData`, which returns `{ data, error, refresh, status }` (`'loading' | 'loaded' | 'error' | 'disabled'`).
|
|
- If the spec changes but the SWR key doesn't, the connection stays bound to the original spec — bump the key to swap specs.
|
|
- In a multi-cluster app, include the cluster in the key and derive it from the same source that built the client — see the note in [../frontend.md](../frontend.md).
|
|
|
|
### useAction
|
|
|
|
Wraps any async function with lifecycle state. Use it for sends, connects, and every other imperative flow instead of `useState` + `try/catch`. The wrapped function receives an `AbortSignal` as its first argument, followed by whatever `dispatch` is called with:
|
|
|
|
```tsx
|
|
const { dispatch, dispatchAsync, data, error, isRunning, reset } = useAction(
|
|
async (signal: AbortSignal, to: Address) => {
|
|
const ix = getTransferSolInstruction({ source: client.payer, destination: to, amount });
|
|
const result = await client.sendTransaction([ix], { abortSignal: signal });
|
|
return result.context.signature;
|
|
},
|
|
);
|
|
```
|
|
|
|
- `dispatch` returns `void` and never throws — the variant for `onClick`. `dispatchAsync` resolves the value or rejects.
|
|
- Dispatching while a call is in flight aborts the first via its `AbortSignal`. Awaiters of the superseded `dispatchAsync` see an `AbortError`, filterable with `isAbortError`, importable directly from `@solana/kit` (7.1+ re-exports `@solana/promises`). Sticking to `dispatch` where you can avoids the question entirely.
|
|
- `data` and `error` persist through subsequent `running` states for stale-while-revalidate UX; only `reset()` clears `data`.
|
|
- `fn` is held in a ref pointing at the latest render's closure — no deps array.
|
|
|
|
Most of the wallet plugin's action hooks (`useConnect`, `useDisconnect`, `useSignIn`, `useSignMessage`) are built on this and expose the same shape.
|
|
|
|
## Client Capability Hooks (requires `@solana/react` 7.1+)
|
|
|
|
These read off whichever plugin capabilities the client advertises. Each takes the client as its only argument.
|
|
|
|
### usePayer / useIdentity
|
|
|
|
`usePayer(client)` reads `client.payer`; `useIdentity(client)` reads `client.identity`. Both return the current `TransactionSigner`, or `undefined` while none is available.
|
|
|
|
```tsx
|
|
const payer = usePayer(client);
|
|
const identity = useIdentity(client);
|
|
return <span>{payer ? `Paying with ${payer.address}` : 'No payer'}</span>;
|
|
```
|
|
|
|
- When the client advertises `subscribeToPayer` / `subscribeToIdentity`, the hook subscribes so the returned signer always reflects the latest value. Otherwise it falls back to a one-time read.
|
|
- **Gotcha:** if reading the underlying value throws — as the wallet plugin does for `payer`/`identity` when it owns those roles and no wallet is connected — the hook surfaces `undefined` rather than throwing.
|
|
|
|
### usePlanTransaction / usePlanTransactions / useSendTransaction / useSendTransactions
|
|
|
|
Wrap a client's transaction planning/sending capabilities as `useAction`-style reactive actions — same `dispatch` / `dispatchAsync` / `data` / `error` / `isRunning` shape as `useAction`.
|
|
|
|
| Hook | Wraps | `dispatch` args | Resolves with |
|
|
|------|-------|------------------|----------------|
|
|
| `usePlanTransaction(client)` | `client.planTransaction` | instruction input | the planned transaction message |
|
|
| `usePlanTransactions(client)` | `client.planTransactions` | instruction input | the full transaction plan (may span multiple transactions) |
|
|
| `useSendTransaction(client)` | `client.sendTransaction` | instructions, an instruction plan, a transaction message, or a transaction plan | the successful single-transaction-plan result |
|
|
| `useSendTransactions(client)` | `client.sendTransactions` | instructions, an instruction plan, or a transaction plan | the transaction plan result for all transactions |
|
|
|
|
```tsx
|
|
const { dispatch, data, isRunning } = useSendTransaction(client);
|
|
<button disabled={isRunning} onClick={() => dispatch(instructions)}>Send</button>
|
|
```
|
|
|
|
Use the singular hooks when you expect everything to fit in one transaction; reach for the plural hooks when instructions might need splitting across transactions.
|
|
|
|
### useAirdrop
|
|
|
|
Wraps a client's `airdrop` capability (`ClientWithAirdrop`) as a tracked `useAction`. `dispatch(address, amount)` requests an airdrop with an injected `AbortSignal` and resolves with the transaction `Signature`, or `undefined` when the airdrop was applied without a transaction (e.g. some local-validator implementations update balances directly, with no transaction to sign).
|
|
|
|
```tsx
|
|
import { useAirdrop } from '@solana/react';
|
|
import { lamports } from '@solana/kit';
|
|
|
|
const { dispatch, isRunning } = useAirdrop(client);
|
|
<button disabled={isRunning} onClick={() => dispatch(address, lamports(1_000_000_000n))}>
|
|
Airdrop 1 SOL
|
|
</button>
|
|
```
|
|
|
|
## Wallet Hooks (`@solana/kit-plugin-wallet/react`)
|
|
|
|
Requires `@solana/kit-plugin-wallet` 0.14+ and the `walletSigner` (or `walletWithoutSigner`) plugin on the client. Every hook takes the wallet-enabled `client` as its first argument, keeping the app fully typed end-to-end.
|
|
|
|
**State hooks:**
|
|
|
|
| Hook | Returns |
|
|
|------|---------|
|
|
| `useWallets(client)` | Discovered Wallet Standard wallets for the configured chain |
|
|
| `useConnectedWallet(client)` | Active connection (`{ account, signer, wallet }`) or `null` |
|
|
| `useWalletStatus(client)` | `'pending' \| 'disconnected' \| 'connecting' \| 'connected' \| 'disconnecting' \| 'reconnecting'` |
|
|
| `useIsWalletReady(client)` | `false` during discovery warm-up, then `true` |
|
|
|
|
**Action hooks** (built on `useAction` — expose `dispatch` + pending/error state):
|
|
|
|
| Hook | Wraps |
|
|
|------|-------|
|
|
| `useConnect(client)` | `client.wallet.connect(wallet)` |
|
|
| `useDisconnect(client)` | `client.wallet.disconnect()` |
|
|
| `useSignIn(client)` | Sign-In-With-Solana (`client.wallet.signIn(wallet, input)`) |
|
|
| `useSignMessage(client)` | `client.wallet.signMessage(message)` |
|
|
|
|
`useSelectAccount(client)` is the exception: switching accounts is synchronous, so it returns the bound `selectAccount(account)` function directly rather than an `ActionResult` — there is no `dispatch` to destructure.
|
|
|
|
**Component:** `WalletReadyGate` — takes `client` as a prop, renders `fallback` until wallet discovery settles.
|
|
|
|
```tsx
|
|
import {
|
|
useConnect,
|
|
useConnectedWallet,
|
|
useWallets,
|
|
WalletReadyGate,
|
|
} from '@solana/kit-plugin-wallet/react';
|
|
import type { ClientWithWallet } from '@solana/kit-plugin-wallet';
|
|
|
|
function WalletPicker({ client }: { client: ClientWithWallet }) {
|
|
const wallets = useWallets(client);
|
|
const connected = useConnectedWallet(client);
|
|
const { dispatch: connect } = useConnect(client);
|
|
|
|
if (connected) return <p>{connected.account.address}</p>;
|
|
return wallets.map((w) => (
|
|
<button key={w.name} onClick={() => connect(w)}>{w.name}</button>
|
|
));
|
|
}
|
|
```
|
|
|
|
## Chain Identifiers
|
|
|
|
```ts
|
|
'solana:mainnet'
|
|
'solana:devnet'
|
|
'solana:testnet'
|
|
'solana:localnet'
|
|
```
|
|
|
|
## Full App Pattern
|
|
|
|
See [../frontend.md](../frontend.md) for the complete Next.js App Router setup (providers, wallet button, transaction sending, data fetching).
|