@solana/react-hooks
v1.4.1
Published
React hooks for the @solana/client Solana client
Readme
@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
npm install @solana/client @solana/react-hooksQuickstart
- Choose wallet connectors (auto-discovery is the fastest way to start).
- Create a Solana client.
- Wrap your tree with
SolanaProviderand use the hooks.
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
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)
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)
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
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
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: truewhen passing raw bigint amounts. For human-readable decimal strings like"1.5", omit the flag.
Available properties:
balance/owner— token balance and owner addressstatus— overall hook status ('disconnected' | 'error' | 'loading' | 'ready')error— error from balance fetchingsend(config, opts?)— transfer tokens to destinationisSending/sendStatus/sendError/sendSignature— transfer staterefresh()/refreshing— manually refresh balanceresetSend()— clear send error statehelper— low-level helper for advanced use
Options (second parameter):
commitment— RPC commitment levelowner— override balance owner (defaults to connected wallet)revalidateOnFocus— refresh when window regains focusswr— additional SWR optionsconfig.tokenProgram— token program:'auto'for detection, or explicit address
Token 2022 support
Token 2022 mints are supported via the tokenProgram config option:
// 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
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
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
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 instructionsremoveInstruction(index)/clearInstructions()/replaceInstructions(ixs)— manage queueinstructions— current instruction queueprepare(opts)/prepareAndSend(opts)— build and optionally sendsend(opts)/sign(opts)— send or sign prepared transactionisPreparing/prepareStatus/prepareError— prepare stateisSending/sendStatus/sendError/sendSignature— send statelatestBlockhash— current blockhash for lifetimeprepared/toWire()— access prepared transactionreset()— clear all state
Simple mutation helper (when you already have instructions)
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
authorityexplicitly.
Available properties:
send(request, opts?)— build and send transactionsendPrepared(prepared, opts?)— send already-prepared transactionisSending/status/signature/error— transaction statereset()— clear state for new transaction
Track confirmations for a signature
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
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
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.
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)
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:truededupingInterval:2000msfocusThrottleInterval:5000ms
SWR background: stale-while-revalidate (RFC 5861): https://datatracker.ietf.org/doc/html/rfc5861
Work with the client store directly
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:
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; usefilterByNames()to filter by name, or explicitly composephantom(),solflare(),backpack(),metamask(), etc. - Queries: all RPC query hooks accept
swroptions underswranddisabledflags. Suspense is opt-in viaSolanaQueryProvider’ssuspenseprop. - Authorities: transaction helpers default to the connected wallet session when
authorityis omitted. - Types: every hook exports
UseHookNameParameters/UseHookNameReturnTypealiases.
More resources
- Documentation — full guides and API reference
- Playground:
examples/vite-react(run withpnpm install && pnpm dev). - Next.js reference app:
examples/nextjs. - Hook JSDoc lives in
src/hooks.ts,src/queryHooks.ts,src/ui.tsx.
