Solana Framework-Kit Tutorial

December 2, 2025

A comprehensive guide to building Solana dApps with Framework-Kit - an evolving multi-framework client library (React-first) that orchestrates wallets, transactions, and reactive data flows.

Introduction

What is Framework-Kit?

Framework-Kit is an experimental, evolving multi-framework Solana client library that provides:

Era Library Key Features
2020-2024: The Legacy Era @solana/web3.js (v1.x) • Monolithic, class-based architecture
• Everything bundled together (can't tree-shake)
• ~311KB/226KB bundle impact even for simple apps
• Connection class with dozens of methods you don't need
• Still works, but archived/maintenance-only
2024-2025: The Modern Foundation @solana/kit (formerly web3.js v2) • Modular, function-based architecture
• Fully tree-shakable (use only what you need)
• Native BigInt, Ed25519, modern JS features
• Type-safe RPC methods
• Low-level primitives only (verbose boilerplate)
2025: The Abstraction Layer Gill • Built on @solana/kit
• Higher-level abstractions (createTransaction, createSolanaClient)
• Reduces boilerplate significantly
• Still requires manual state management
2025+: The Reactive Framework Framework-Kit • Built on modern foundations
• Reactive state management (Zustand + SWR)
• Framework bindings (React hooks, providers)
• Real-time subscriptions built-in
• Complete solution for building dApps

Package Overview

Package Description
@solana/client Headless Solana client - works in any runtime
@solana/react-hooks React hooks and providers powered by the client
@solana/web3-compat Migration layer for existing @solana/web3.js code

Installation

Prerequisites

Install Packages

# Using pnpm (recommended)
pnpm add @solana/client @solana/react-hooks

# Using npm
npm install @solana/client @solana/react-hooks

# For web3.js migration compatibility
pnpm add @solana/web3-compat

Getting Started

Basic React Setup

The quickest way to get started is with React:

import { createClient, autoDiscover } from "@solana/client";
import { SolanaProvider, useWalletConnection } from "@solana/react-hooks";

// 1. Create a Solana client with wallet connectors
const client = createClient({
  endpoint: "https://api.devnet.solana.com",
  walletConnectors: autoDiscover(),
});

// 2. Create wallet connection buttons
function WalletButtons() {
  const { connectors, connect, disconnect, wallet, status } = useWalletConnection();

  if (status === "connected") {
    return (
      <div>
        <p>Connected: {wallet?.account.address.toString()}</p>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    );
  }

  return (
    <>
      {connectors.map((connector) => (
        <button key={connector.id} onClick={() => connect(connector.id)}>
          Connect {connector.name}
        </button>
      ))}
    </>
  );
}

// 3. Wrap your app with SolanaProvider
export function App() {
  return (
    <SolanaProvider client={client}>
      <WalletButtons />
    </SolanaProvider>
  );
}

Headless Client Usage (No React)

The @solana/client package works independently of React:

import { createClient, phantom, solflare, backpack, autoDiscover } from "@solana/client";

// Configure wallet connectors
const walletConnectors = [...phantom(), ...solflare(), ...backpack(), ...autoDiscover()];

const client = createClient({
  endpoint: "https://api.devnet.solana.com",
  websocketEndpoint: "wss://api.devnet.solana.com",
  walletConnectors,
});

// Connect a wallet
await client.actions.connectWallet("phantom");

// Fetch account data
const account = await client.actions.fetchAccount(address);
console.log("Balance:", account.lamports?.toString());

// Watch balance changes in real-time
const watcher = client.watchers.watchBalance({ address }, (lamports) => {
  console.log("New balance:", lamports.toString());
});

// Stop watching when done
watcher.abort();

Core Concepts

Client Store (State Management)

Framework-Kit uses a Zustand store to manage all state. The store contains:

type ClientState = {
  cluster: {
    endpoint: string;
    websocketEndpoint?: string;
    commitment: "processed" | "confirmed" | "finalized";
    status: { status: "idle" | "connecting" | "ready" | "error"; latencyMs?: number };
  };
  wallet: {
    status: "disconnected" | "connecting" | "connected" | "error";
    session?: WalletSession;
    connectorId?: string;
  };
  accounts: {
    [address: string]: {
      address: Address;
      lamports: bigint | null;
      data?: unknown;
      fetching: boolean;
      slot: bigint | null;
    };
  };
  transactions: {
    [id: string]: {
      signature?: string;
      status: "idle" | "sending" | "waiting" | "confirmed" | "failed";
      error?: unknown;
    };
  };
  subscriptions: { account: Record<string, SubscriptionStatus>; signature: Record<string, SubscriptionStatus> };
  lastUpdatedAt: number;
};

Actions

Actions are Promise-based functions that interact with the Solana network:

Action Description
connectWallet(connectorId) Connect a wallet via Wallet Standard
disconnectWallet() Disconnect the active wallet
fetchAccount(address) One-time account data fetch
fetchBalance(address) Get lamport balance
sendTransaction(tx) Submit a transaction
setCluster(endpoint) Switch RPC endpoint
requestAirdrop(address, lamports) Request devnet SOL

Watchers (Subscriptions)

Watchers subscribe to real-time updates:

Watcher Description
watchBalance(config, callback) Subscribe to balance changes
watchAccount(config, callback) Subscribe to account data changes
watchSignature(signature, callback) Track transaction confirmation

Helpers

Pre-built utilities for common operations:


React Integration

Provider Setup

Wrap your app with SolanaProvider:

import { createClient, autoDiscover } from "@solana/client";
import { SolanaProvider } from "@solana/react-hooks";

const client = createClient({
  endpoint: "https://api.devnet.solana.com",
  walletConnectors: autoDiscover(),
});

export function App() {
  return (
    <SolanaProvider client={client}>
      {/* Your components go here */}
    </SolanaProvider>
  );
}

Available Hooks

Wallet Hooks

Hook Purpose
useWallet() Access wallet status and session
useWalletConnection() Connect, disconnect, list connectors
useWalletSession() Get current wallet session

Data Hooks

Hook Purpose
useBalance(address) Reactive balance tracking
useAccount(address) Account data with auto-refresh

Transaction Hooks

Hook Purpose
useSolTransfer() Send SOL easily
useSplToken(mint) SPL token balance and transfers
useSendTransaction() Send arbitrary transactions
useTransactionPool() Build complex transactions

Query Hooks

Hook Purpose
useLatestBlockhash() Get current blockhash
useProgramAccounts(program) Query program accounts
useSimulateTransaction(wire) Simulate transactions
useSignatureStatus(signature) Check transaction status
useWaitForSignature(signature) Wait for confirmation

Hook Examples

Connect, Disconnect, and Show Balance

import { useWalletConnection, useBalance } from "@solana/react-hooks";

function WalletPanel() {
  const { connectors, connect, disconnect, wallet, status } = useWalletConnection();
  const address = wallet?.account.address;
  const balance = useBalance(address);

  if (status === "connected") {
    return (
      <div>
        <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>
  );
}

Send SOL

import { useSolTransfer } from "@solana/react-hooks";

function SendSol({ destination }: { destination: string }) {
  const { send, isSending, status, signature, error } = useSolTransfer();

  return (
    <div>
      <button
        disabled={isSending}
        onClick={() => send({ destination, lamports: 100_000_000n /* 0.1 SOL */ })}
      >
        {isSending ? "Sending..." : "Send 0.1 SOL"}
      </button>
      <p>Status: {status}</p>
      {signature && <p>Signature: {signature}</p>}
      {error && <p role="alert">Error: {String(error)}</p>}
    </div>
  );
}

SPL Token Balance + Transfer

import { useSplToken } from "@solana/react-hooks";

function TokenPanel({ mint, destinationOwner }: { mint: string; destinationOwner: string }) {
  const { balance, send, isSending, owner } = useSplToken(mint);

  return (
    <div>
      <p>Owner: {owner ?? "Connect wallet"}</p>
      <p>Balance: {balance?.uiAmount ?? "0"}</p>
      <button
        disabled={isSending || !owner}
        onClick={() => send({ amount: 1n, destinationOwner })}
      >
        {isSending ? "Sending..." : "Send 1 token"}
      </button>
    </div>
  );
}

Build and Send Arbitrary Transactions

import type { TransactionInstructionInput } from "@solana/client";
import { useTransactionPool } from "@solana/react-hooks";

function TransactionFlow({ ix }: { ix: TransactionInstructionInput }) {
  const pool = useTransactionPool();

  return (
    <div>
      <button onClick={() => pool.addInstruction(ix)}>Add instruction</button>
      <button disabled={pool.isSending} onClick={() => pool.prepareAndSend()}>
        {pool.isSending ? "Sending..." : "Prepare & Send"}
      </button>
      <p>Blockhash: {pool.latestBlockhash.blockhash ?? "loading..."}</p>
    </div>
  );
}

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>
  );
}

// Wrap with SolanaQueryProvider
function ProgramAccountsSection({ program }: { program: string }) {
  return (
    <SolanaQueryProvider>
      <ProgramAccounts program={program} />
    </SolanaQueryProvider>
  );
}

Using Suspense (Opt-in)

Enable Suspense per subtree for cleaner loading states:

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)

Customize the SWR query layer:

export function App() {
  return (
    <SolanaProvider
      client={client}
      query={{
        config: {
          revalidateOnFocus: false,
          revalidateOnReconnect: false,
          refreshInterval: 30_000,
        },
      }}
    >
      <WalletPanel />
    </SolanaProvider>
  );
}

Default SWR settings:

Access the Client Store Directly

import { useClientStore } from "@solana/react-hooks";

function ClusterBadge() {
  const cluster = useClientStore((s) => s.cluster);
  return <p>Endpoint: {cluster.endpoint}</p>;
}

Next.js Integration

Step 1: Configure Next.js

Create or update next.config.mjs:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  transpilePackages: [
    "@solana/client",
    "@solana/react-hooks",
    "@solana/web3-compat",
    "@solana/kit",
  ],
};

export default nextConfig;

Step 2: Create a Client Provider

Create app/providers.tsx:

'use client';

import type { SolanaClientConfig } from '@solana/client';
import { SolanaProvider } from '@solana/react-hooks';
import type { PropsWithChildren } from 'react';

const defaultConfig: SolanaClientConfig = {
  cluster: 'devnet',
  rpc: 'https://api.devnet.solana.com',
  websocket: 'wss://api.devnet.solana.com',
};

function Providers({ children }: PropsWithChildren) {
  return <SolanaProvider config={defaultConfig}>{children}</SolanaProvider>;
}

export default Providers;

Step 3: Wrap Your Layout

Update app/layout.tsx:

import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import Providers from './providers';
import './globals.css';

export const metadata: Metadata = {
  title: 'My Solana App',
  description: 'Built with Framework-Kit',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Step 4: Use Hooks in Client Components

Create client components that use hooks:

// app/components/wallet-panel.tsx
'use client';

import { useWalletConnection, useBalance } from '@solana/react-hooks';

export function WalletPanel() {
  const { connectors, connect, disconnect, wallet, status } = useWalletConnection();
  const address = wallet?.account.address;
  const balance = useBalance(address);

  if (status === 'connected') {
    return (
      <div>
        <p>Connected: {address?.toString()}</p>
        <p>Balance: {balance.lamports?.toString() ?? 'Loading...'} lamports</p>
        <button onClick={disconnect}>Disconnect</button>
      </div>
    );
  }

  return (
    <div>
      <h3>Connect a Wallet</h3>
      {connectors.map((c) => (
        <button key={c.id} onClick={() => connect(c.id)}>
          {c.name}
        </button>
      ))}
    </div>
  );
}

Important Next.js Notes

  1. Mark client components with 'use client' - All components using hooks must be client components
  2. SolanaProvider must be in a client component - Create a separate providers file
  3. No server-side hooks - Use @solana/client directly in API routes for server-side operations
  4. Environment variables - Use NEXT_PUBLIC_ prefix for client-side access:
    NEXT_PUBLIC_SOLANA_RPC=https://api.devnet.solana.com
    NEXT_PUBLIC_SOLANA_WS=wss://api.devnet.solana.com
    

Features & API Reference

Transaction Helper

The transaction helper handles blockhashes, fee payers, and signing:

// Prepare a transaction
const prepared = await client.helpers.transaction.prepare({
  authority: walletSession,
  instructions: [instruction],
});

// Send the prepared transaction
const signature = await client.helpers.transaction.send(prepared);
console.log(signature.toString());

Methods:

SOL Transfer Helper

// Prepare and send SOL
const signature = await client.solTransfer.prepareAndSend({
  authority: walletSession,
  destination: recipientAddress,
  amount: 100_000_000n, // 0.1 SOL
});

SPL Token Helper

// Transfer SPL tokens (auto-creates ATA if needed)
const signature = await client.splToken.prepareAndSend({
  mint: tokenMintAddress,
  authority: walletSession,
  destinationOwner: recipientAddress,
  amount: 100n,
  ensureDestinationAta: true, // Auto-create ATA
});

Wallet Connectors

import { phantom, solflare, backpack, autoDiscover } from "@solana/client";

// Combine specific and auto-discovered wallets
const walletConnectors = [
  ...phantom(),       // Phantom wallet
  ...solflare(),      // Solflare wallet
  ...backpack(),      // Backpack wallet
  ...autoDiscover(),  // All Wallet Standard wallets
];

const client = createClient({
  endpoint: "https://api.devnet.solana.com",
  walletConnectors,
});

Examples

The repository includes two example applications:

Vite + React Example

Location: examples/vite-react/

Features demonstrated:

Run it:

pnpm install
pnpm --filter @solana/example-vite-react dev
# Opens at http://localhost:5173

Next.js Example

Location: examples/nextjs/

Features demonstrated:

Run it:

pnpm install
pnpm --filter @solana/example-nextjs dev
# Opens at http://localhost:3000

Configuration Options

Client Configuration

type SolanaClientConfig = {
  // RPC Configuration
  endpoint?: string;           // HTTP endpoint
  rpc?: string;                // Alias for endpoint
  websocketEndpoint?: string;  // WebSocket endpoint
  websocket?: string;          // Alias for websocketEndpoint
  cluster?: "mainnet" | "testnet" | "devnet" | "localnet";

  // Commitment Level
  commitment?: "processed" | "confirmed" | "finalized";

  // Wallet Connectors
  walletConnectors?: WalletConnector[];

  // Custom Store or RPC Client
  createStore?: (state: ClientState) => ClientStore;
  rpcClient?: SolanaRpcClient;

  // Logging
  logger?: (event: { level: string; message: string; data?: Record<string, any> }) => void;

  // State Persistence
  initialState?: SerializableSolanaState;
};

SolanaProvider Props

<SolanaProvider
  client={client}                    // Pre-created client (optional)
  config={config}                    // Client config (if no client)
  query={{
    config: swrConfig,               // SWR configuration
    suspense: true,                  // Enable Suspense
    resetOnClusterChange: true,      // Clear cache on cluster switch
    disabled: false,                 // Disable query layer
  }}
  walletPersistence={{
    autoConnect: true,               // Auto-connect on load
    storage: localStorage,           // Custom storage
    storageKey: "solana:last-connector",
  }}
>
  {children}
</SolanaProvider>

Transaction Configuration

type TransactionPrepareRequest = {
  instructions: readonly TransactionInstruction[];
  authority?: TransactionSigner | WalletSession;
  feePayer?: Address | string | TransactionSigner;
  commitment?: Commitment;
  computeUnitLimit?: bigint | number;
  computeUnitPrice?: bigint | number;
  lifetime?: { blockhash; lastValidBlockHeight };
  version?: "legacy" | "0" | "auto";
  abortSignal?: AbortSignal;
};

Troubleshooting

Common Issues

"Cannot find module '@solana/client'"

Make sure you've installed the packages:

pnpm add @solana/client @solana/react-hooks

For Next.js, ensure transpilePackages is configured in next.config.mjs.

Hooks not working / "Invalid hook call"

Wallet not connecting

Transaction failing

Development Scripts

# Install dependencies
pnpm install

# Build all packages
pnpm build

# Start development servers
pnpm dev

# Run tests
pnpm test

# Type checking
pnpm typecheck

# Linting and formatting
pnpm lint
pnpm format

Summary

Framework-Kit provides a modern, reactive approach to building Solana dApps:

  1. Install packages - @solana/client and @solana/react-hooks
  2. Create a client - Configure endpoint and wallet connectors
  3. Wrap with SolanaProvider - Enable hooks throughout your app
  4. Use hooks - useWalletConnection, useBalance, useSolTransfer, etc.
  5. Build your dApp - Leverage reactive data flows and transaction helpers

The framework handles the complexity of wallet orchestration, state management, and transaction building so you can focus on building great user experiences.