learn.sol
Solana Kit Clients • Kit Foundations
Lesson 2 of 11

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.

Client RuntimeTap to reveal
The shared app-level client object that owns endpoints, wallet connectors, subscriptions, and common transaction behavior.
Client Runtime
SolanaProviderTap to reveal
The React boundary that exposes the shared runtime to hooks instead of recreating transport logic per page.
SolanaProvider
autoDiscoverTap to reveal
The connector discovery step that finds Wallet Standard wallets available in the browser.
autoDiscover
Network ConfigTap to reveal
The one central place where RPC and WebSocket endpoints are defined so the app cannot drift across clusters.
Network Config

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-query

You need:

  • @solana/client for the shared runtime
  • @solana/react-hooks for React integration
  • @solana/kit for typed transaction and RPC building blocks used elsewhere in this module
  • @tanstack/react-query if 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:

  1. createClient(...) builds the shared runtime
  2. autoDiscover() registers Wallet Standard connectors available in the browser
  3. SolanaProvider makes that runtime available to hooks
  4. QueryClientProvider keeps 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

Quick Check
Single answer

Why should a kit-first app create one shared client runtime at the app boundary instead of per page?

Quick Check
Single answer

What bug does centralizing `SOLANA_RPC` and `SOLANA_WS` help prevent?

Sources

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px