Kit Foundations
Set up one shared Solana client runtime so your app reads state, manages wallet sessions, and sends transactions without duplicating transport logic.
@solana/web3.js is now the legacy path for new frontend work.
The modern client stack is not just a package swap.
It changes the architecture.
The old mistake was letting every page create its own RPC connection, wallet glue, and transaction helpers.
That leads to inconsistent cluster state, duplicated subscriptions, and UI bugs that are hard to explain.
The correct model is simpler.
Your app should create one shared Solana client runtime and let everything else sit on top of it.
What The Shared Runtime Actually Owns
A proper Solana client runtime owns these responsibilities:
- RPC endpoint selection
- WebSocket subscriptions
- wallet connector registry
- wallet session state
- transaction helpers and watchers
That is what @solana/client gives you.
Then @solana/react-hooks exposes React hooks on top of the same runtime.
That split is the key idea for this whole section.
Install The Client Stack Once
pnpm add @solana/client @solana/react-hooks @solana/kit @tanstack/react-queryYou need:
@solana/clientfor the shared runtime@solana/react-hooksfor React integration@solana/kitfor typed transaction and RPC building blocks used elsewhere in this module@tanstack/react-queryif you want query state to stay predictable across the app
Step 1: Centralize Network Configuration
Create lib/solana/config.ts:
export const SOLANA_RPC =
process.env.NEXT_PUBLIC_SOLANA_RPC ?? "https://api.devnet.solana.com";
export const SOLANA_WS =
process.env.NEXT_PUBLIC_SOLANA_WS ?? "wss://api.devnet.solana.com";This file should stay boring.
That is the point.
If cluster configuration is spread across random files, you will eventually read from one cluster and send to another.
Step 2: Create One Client Instance
Now create app/providers.tsx:
"use client";
import React from "react";
import { autoDiscover, createClient } from "@solana/client";
import { SolanaProvider } from "@solana/react-hooks";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SOLANA_RPC, SOLANA_WS } from "@/lib/solana/config";
const queryClient = new QueryClient();
const client = createClient({
endpoint: SOLANA_RPC,
websocketEndpoint: SOLANA_WS,
walletConnectors: autoDiscover(),
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<SolanaProvider client={client}>{children}</SolanaProvider>
</QueryClientProvider>
);
}Read this file in the right order:
createClient(...)builds the shared runtimeautoDiscover()registers Wallet Standard connectors available in the browserSolanaProvidermakes that runtime available to hooksQueryClientProviderkeeps async UI state stable around it
That is the real foundation.
Not the UI.
The runtime.
Step 3: Wrap The App Once
In app/layout.tsx:
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Do this once.
Do not recreate the client per page.
Do not create a different provider subtree for every feature.
One runtime is the clean default.
Why This Architecture Matters
The official docs describe one client instance exposing store, actions, watchers, and helpers.
That matters because it gives your app one shared truth for:
- current wallet session
- current endpoint
- account watchers
- pending transaction state
If each page rolls its own transport logic, those truths drift apart.
That is when frontend bugs start to feel random.
They are not random.
They are architecture problems.
The Failure Modes To Avoid
Recreating the client inside components
That resets transport and wallet-related state far more often than you think.
Keep client creation at the app boundary.
Hardcoding a different endpoint in one feature
That produces split-brain behavior.
One screen reads devnet while another flow sends somewhere else.
Treating hooks as magic
The hooks are just a React interface over the shared client runtime.
If the runtime is wrong, the hooks will still fail correctly.
What You Can Do After This
You can explain where wallet sessions, RPC state, and transaction helpers actually live in a kit-first app.
That is the baseline for every other client lesson.
The next lesson moves one layer up and shows how wallet connection works on top of this shared runtime.
Quick Check
Why should a kit-first app create one shared client runtime at the app boundary instead of per page?
What bug does centralizing `SOLANA_RPC` and `SOLANA_WS` help prevent?
Up Next in Solana Kit Clients
Wallet Standard Connection Flow
Use the shared runtime honestly by showing real wallet session state instead of letting the UI guess what connected means.