Files

12 KiB

title, description
title description
React Reference 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:

// 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:

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.

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.

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:

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.

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
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).

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.

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

'solana:mainnet'
'solana:devnet'
'solana:testnet'
'solana:localnet'

Full App Pattern

See ../frontend.md for the complete Next.js App Router setup (providers, wallet button, transaction sending, data fetching).