---
title: Frontend with Solana Kit
description: Build React and Next.js Solana apps with a Kit plugin client, Wallet Standard connection via @solana/kit-plugin-wallet (+ its React hooks), and @solana/react client bindings.
---
# Frontend with Solana Kit (Next.js / React)
## Goals
- One Kit client instance for the app (RPC + wallet + transaction sending)
- Wallet Standard-first discovery/connect (no wallet-specific adapters)
- Minimal "use client" footprint in Next.js (hooks only in leaf components)
- Transaction sending that is observable, cancelable, and UX-friendly
## Recommended dependencies
- `@solana/kit` (v7+)
- `@solana/kit-plugin-rpc`, `@solana/kit-plugin-wallet` (wallet React hooks ship in `@solana/kit-plugin-wallet/react`)
- `@solana/react` (v7+ — Kit client bindings: `ClientProvider`, `useClient`, data hooks)
- `swr` (peer dep of `@solana/react/swr`) or `@tanstack/react-query` (peer dep of `@solana/react/query`) — pick whichever your app already uses
- `@solana-program/system`, `@solana-program/token`, `@your-program/codama-client` etc. (only what you need)
`solanaRpc` already bundles transaction planning/sending — you do not need `@solana/kit-plugin-instruction-plan` in apps.
Do **not** use `@solana/client` / `@solana/react-hooks` (framework-kit) for new work — that stack is stale; the maintained path is Kit plugins + `@solana/react`. Do not use `@solana/wallet-adapter-*` for new apps either; Wallet Standard discovery covers modern wallets.
## Bootstrap recommendation
Prefer `create-solana-dapp` and pick a Kit template for new projects.
## Client setup (Next.js App Router)
Create a single wallet-backed client, export its type, and provide it via `ClientProvider` from `@solana/react`.
Example `app/providers.tsx`:
```tsx
'use client';
import React from 'react';
import { createClient } from '@solana/kit';
import { solanaRpc } from '@solana/kit-plugin-rpc';
import { walletSigner } from '@solana/kit-plugin-wallet';
import { ClientProvider } from '@solana/react';
const rpcUrl =
process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.devnet.solana.com';
// One client for the whole app. The connected wallet fills payer + identity.
export const client = createClient()
.use(walletSigner({ chain: 'solana:devnet' }))
.use(solanaRpc({ rpcUrl }));
// Export the client type so every useClient() call in the app
// is fully typed (rpc, wallet, sendTransaction, ...).
export type AppClient = Awaited;
export function Providers({ children }: { children: React.ReactNode }) {
return {children};
}
```
Then wrap `app/layout.tsx` with ``.
## Wallet connection
Use the React hooks from `@solana/kit-plugin-wallet/react` — state hooks (`useWallets`, `useConnectedWallet`, `useWalletStatus`, `useIsWalletReady`), action hooks (`useConnect`, `useDisconnect`, `useSignIn`, `useSignMessage`), `useSelectAccount` (synchronous — returns the bound function, not an action), plus a `WalletReadyGate` component for the discovery warm-up. Every hook takes the wallet-enabled `client` as its first argument:
```tsx
'use client';
import {
useConnect,
useConnectedWallet,
useDisconnect,
useWallets,
WalletReadyGate,
} from '@solana/kit-plugin-wallet/react';
import type { AppClient } from './providers';
function WalletButton({ client }: { client: AppClient }) {
const wallets = useWallets(client);
const connected = useConnectedWallet(client);
const { dispatch: connect } = useConnect(client);
const { dispatch: disconnect } = useDisconnect(client);
if (!connected) {
return wallets.map((wallet) => (
));
}
return (
}>
);
```
Outside React (or for imperative flows), the same state is on the client: `client.wallet.getState()` returns `{ wallets, connected, status }` and `client.wallet.connect(wallet)` / `disconnect()` / `selectAccount(account)` drive the connection.
## Sending transactions
With the wallet plugin installed, `client.sendTransaction` plans, asks the wallet to sign, and sends. Wrap it in `useAction` from `@solana/react` so pending/error state, abort-on-resend, and stale-while-revalidate come for free instead of hand-rolled `useState`:
```tsx
'use client';
import { address, sol, solToLamports } from '@solana/kit';
import { getTransferSolInstruction } from '@solana-program/system';
import { useAction, useClient } from '@solana/react';
import type { AppClient } from '@/app/providers';
function TipButton({ to }: { to: string }) {
const client = useClient();
const { dispatch, isRunning, error, data: signature } = useAction(
async (signal: AbortSignal, recipient: string) => {
const ix = getTransferSolInstruction({
source: client.payer,
destination: address(recipient),
// `sol()` returns a fixed-point value; instructions take Lamports
amount: solToLamports(sol('0.01')),
});
// Forward the signal so a superseded send is actually cancelled
const result = await client.sendTransaction([ix], { abortSignal: signal });
return result.context.signature;
},
);
return (
<>
{signature ? View : null}
>
);
}
```
Use `dispatch` in event handlers — it returns `void` and never throws, so it can't produce an unhandled rejection. Full hook semantics are in [kit/react.md](kit/react.md#useaction).
## Data fetching and subscriptions
`@solana/react` ships data hooks — `useRequest` (one-shot reads), `useSubscription` (websocket streams), `useTrackedData` (a one-shot read seeded into a subscription, slot-deduped), and `useAction` — plus adapters for SWR (`@solana/react/swr`) and TanStack Query (`@solana/react/query`). Prefer these over hand-rolled polling, and always call `useClient()` with your exported client type. See [kit/react.md](kit/react.md#data-hooks) for the per-hook return shapes and gotchas.
**Balance and other live account data: use `useTrackedDataSWR`.** It fires the initial RPC fetch and the account subscription together, slot-dedupes them so an out-of-order arrival never regresses the displayed value, and routes the result through SWR's cache so every component on the same key shares one connection. A Next.js-shaped hook:
```tsx
'use client';
import { useMemo } from 'react';
import { type Address, type Lamports } from '@solana/kit';
import { useClient } from '@solana/react';
import { useTrackedDataSWR } from '@solana/react/swr';
import type { AppClient } from '@/app/providers';
export function useBalance(accountAddress?: Address) {
const { rpc, rpcSubscriptions } = useClient();
// Passing `null` when there is no address is what gates the hook off —
// it must be memoized, since spec identity drives teardown and re-run.
const spec = useMemo(
() =>
accountAddress
? {
initialValueSource: rpc.getBalance(accountAddress, { commitment: 'confirmed' }),
initialValueMapper: (lamports: Lamports) => lamports,
streamSource: rpcSubscriptions.accountNotifications(accountAddress, {
commitment: 'confirmed',
}),
streamValueMapper: ({ lamports }: { lamports: Lamports }) => lamports,
}
: null,
[rpc, rpcSubscriptions, accountAddress],
);
const { data, error } = useTrackedDataSWR(
accountAddress ? ['balance', accountAddress] : null,
spec,
);
return {
lamports: data?.value ?? null,
isLoading: accountAddress != null && data == null && error == null,
error,
};
}
```
**Multi-cluster apps:** include the cluster in the cache key, and derive it from the same source that built the client — the client is typically rebuilt one render *after* the selection flips, so a key read from selection state binds the new network's fetch to the previous network's `rpc`. The [Kit example app](https://github.com/anza-xyz/kit/blob/main/examples/react-app/src/components/Balance.tsx) stamps `chain` onto the client with `extendClient` and reads it back off `useClient()` to keep the two in lockstep.
Render lamports with the Kit helpers rather than dividing by `1e9`:
```tsx
import { formatDecimalFixedPoint, lamportsToSol } from '@solana/kit';
const solFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 5 });
function BalanceDisplay({ accountAddress }: { accountAddress: Address }) {
const { lamports } = useBalance(accountAddress);
if (lamports == null) return –;
return {formatDecimalFixedPoint(solFormatter, lamportsToSol(lamports))} ◎;
}
```
For Next.js: keep server components server-side; only leaf components that call hooks should be client components. Server-side reads can use a plain Kit RPC client (no wallet plugin).
## Transaction UX checklist
- Disable inputs while a transaction is pending
- Provide a signature immediately after send
- Track confirmation states (processed/confirmed/finalized) based on UX need
- Show actionable errors:
- user rejected signing
- insufficient SOL for fees / rent
- blockhash expired / dropped
- account already in use / already initialized
- program error (custom error code)
## Legacy apps
- App built on web3.js v1 + wallet-adapter? Migrate to web3.js v3 (Kit internals, same classes; currently RC) first — see [kit-web3-interop.md](kit-web3-interop.md) for routing to the official migration skill — then adopt Kit plugins incrementally.
- Found `@solana/client` / `@solana/react-hooks` (framework-kit)? Migrate to the Kit plugin client + `@solana/react`: `createClient({ endpoint, walletConnectors })` becomes `createClient().use(walletSigner(...)).use(solanaRpc(...))`, and framework-kit hooks map to the `@solana/kit-plugin-wallet/react` hooks or `client.wallet` state.