From 42bc2cb80a6f99c893b7f0e3bdfb491866428219 Mon Sep 17 00:00:00 2001 From: guibibeau Date: Thu, 27 Nov 2025 00:42:09 +0700 Subject: [PATCH 01/10] docs: update Next.js frontend guide to use @solana/react-hooks Replace @solana/kit with the newer, more convenient @solana/react-hooks package --- apps/web/content/docs/en/frontend/kit.mdx | 830 +++++++--------------- 1 file changed, 257 insertions(+), 573 deletions(-) diff --git a/apps/web/content/docs/en/frontend/kit.mdx b/apps/web/content/docs/en/frontend/kit.mdx index 5414724c5..5a93a4a9d 100644 --- a/apps/web/content/docs/en/frontend/kit.mdx +++ b/apps/web/content/docs/en/frontend/kit.mdx @@ -1,678 +1,362 @@ --- -title: Next.js - Solana Kit -seoTitle: Solana wallet integration with Next.js and Solana Kit -description: Set up Solana wallet integration in Next.js with Solana Kit. +title: Next.js + Solana React Hooks +seoTitle: Solana wallet integration with Next.js and @solana/react-hooks +description: Set up Solana wallet integration in Next.js with @solana/client and @solana/react-hooks. --- -Set up a minimal Solana wallet integration in Next.js with Solana Kit. +Set up a minimal Solana wallet integration in Next.js (App Router) with `@solana/client` and +`@solana/react-hooks`. You'll create a connect wallet dropdown and a memo-sending component. -This guide provides a minimal example of implementing Solana wallet -functionality in a Next.js application using `@solana/kit`. You'll create a -connect wallet button and a component to send transactions. +![Next.js Hooks App](/assets/docs/frontend/nextjs-hooks.gif) -![Nextjs Kit Application](/assets/docs/frontend/nextjs-kit.gif) - -For a more comprehensive example of using `@solana/kit` in a React application, -refer to the -[React App Example](https://github.com/anza-xyz/kit/tree/main/examples/react-app) -in the Solana Kit repository. + ## Resources -- [Solana Kit Documentation](https://www.solanakit.com/docs) -- [Solana Kit GitHub Repository](https://github.com/anza-xyz/kit) +- [Framework Kit Repository](https://github.com/solana-foundation/framework-kit) +- [Solana JSON RPC](https://docs.solana.com/cluster/rpc-endpoints) ## Prerequisites -- Install [Node.js](https://nodejs.org/en/download) +- Node 20+ +- pnpm ## Create Next.js Project -Create a new Next.js project with [shadcn](https://ui.shadcn.com/) for ui -components and install required Solana dependencies. - -```terminal -$ npx shadcn@latest init -``` - -Navigate to your project directory: - -```terminal -$ cd -``` - -### Install UI Components - -Install the following shadcn ui components: - ```terminal -$ npx shadcn@latest add button dropdown-menu avatar +$ pnpm create next-app my-app +$ cd my-app ``` -### Install Solana Dependencies - -Install the following Solana dependencies: +### Install Solana dependencies ```terminal -$ npm install @solana/kit @solana/react @wallet-standard/core @wallet-standard/react @solana-program/memo +$ pnpm add @solana/client @solana/react-hooks ``` -## Implementation Walkthrough - -Follow the steps below and copy the provided code to your project. - - - -## !!steps 1. Create Solana Context - -First, create a React Context that manages the entire wallet state for the -application. +### Tailwind (optional, lightweight) -Create `components/solana-provider.tsx` and add the provided code. This provider -component will: +Add `@import "tailwindcss";` and `@source "./app/**/*.{ts,tsx}"` to `app/globals.css`. Keep styles minimal—this example +uses a few small utility classes defined there (`shell`, `card`, `btn`, `input`). -- Connect to Solana's devnet RPC endpoints -- Filter for available Solana wallets installed in the user's browser -- Track which wallet and account is currently connected -- Provide wallet state to child components +## !!steps 1. Create Solana Provider - +`app/providers.tsx` uses the simplified `SolanaProvider` props—just set the cluster (Devnet by default). -```tsx !! title="components/solana-provider.tsx" +```tsx !! title="app/providers.tsx" "use client"; -import React, { createContext, useContext, useState, useMemo } from "react"; -import { - useWallets, - type UiWallet, - type UiWalletAccount -} from "@wallet-standard/react"; -import { createSolanaRpc, createSolanaRpcSubscriptions } from "@solana/kit"; -import { StandardConnect } from "@wallet-standard/core"; - -// Create RPC connection -const RPC_ENDPOINT = "https://api.devnet.solana.com"; -const WS_ENDPOINT = "wss://api.devnet.solana.com"; -const chain = "solana:devnet"; -const rpc = createSolanaRpc(RPC_ENDPOINT); -const ws = createSolanaRpcSubscriptions(WS_ENDPOINT); - -interface SolanaContextState { - // RPC - rpc: ReturnType; - ws: ReturnType; - chain: typeof chain; - - // Wallet State - wallets: UiWallet[]; - selectedWallet: UiWallet | null; - selectedAccount: UiWalletAccount | null; - isConnected: boolean; - - // Wallet Actions - setWalletAndAccount: ( - wallet: UiWallet | null, - account: UiWalletAccount | null - ) => void; -} - -const SolanaContext = createContext(undefined); +import type { SolanaClientConfig } from "@solana/client"; +import { SolanaProvider } from "@solana/react-hooks"; -export function useSolana() { - const context = useContext(SolanaContext); - if (!context) { - throw new Error("useSolana must be used within a SolanaProvider"); - } - return context; -} +const defaultConfig: SolanaClientConfig = { + cluster: "devnet", + rpc: "https://api.devnet.solana.com", + websocket: "wss://api.devnet.solana.com", +}; -export function SolanaProvider({ children }: { children: React.ReactNode }) { - const allWallets = useWallets(); - - // Filter for Solana wallets only that support signAndSendTransaction - const wallets = useMemo(() => { - return allWallets.filter( - (wallet) => - wallet.chains?.some((c) => c.startsWith("solana:")) && - wallet.features.includes(StandardConnect) && - wallet.features.includes("solana:signAndSendTransaction") - ); - }, [allWallets]); - - // State management - const [selectedWallet, setSelectedWallet] = useState(null); - const [selectedAccount, setSelectedAccount] = - useState(null); - - // Check if connected (account must exist in the wallet's accounts) - const isConnected = useMemo(() => { - if (!selectedAccount || !selectedWallet) return false; - - // Find the wallet and check if it still has this account - const currentWallet = wallets.find((w) => w.name === selectedWallet.name); - return !!( - currentWallet && - currentWallet.accounts.some( - (acc) => acc.address === selectedAccount.address - ) - ); - }, [selectedAccount, selectedWallet, wallets]); - - const setWalletAndAccount = ( - wallet: UiWallet | null, - account: UiWalletAccount | null - ) => { - setSelectedWallet(wallet); - setSelectedAccount(account); - }; - - // Create context value - const contextValue = useMemo( - () => ({ - // Static RPC values - rpc, - ws, - chain, - - // Dynamic wallet values - wallets, - selectedWallet, - selectedAccount, - isConnected, - setWalletAndAccount - }), - [wallets, selectedWallet, selectedAccount, isConnected] - ); - - return ( - - {children} - - ); +export default function Providers({ children }: { children: React.ReactNode }) { + return {children}; } ``` -```tsx !! title="app/layout.tsx" -// default layout -``` +## !!steps 2. Wallet Connect Button -```tsx !! title="app/page.tsx" -// default page -``` - -```tsx !! title="components/ui/" -// shadcn ui components -``` - -```json !! title="package.json" -{ - "name": "my-app", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build --turbopack", - "start": "next start", - "lint": "eslint" - }, - "dependencies": { - "@radix-ui/react-avatar": "^1.1.10", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-slot": "^1.2.3", - "@solana-program/memo": "^0.8.0", - "@solana/kit": "^3.0.3", - "@solana/react": "^3.0.3", - "@wallet-standard/core": "^1.1.1", - "@wallet-standard/react": "^1.0.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.544.0", - "next": "15.5.4", - "react": "19.1.0", - "react-dom": "19.1.0", - "tailwind-merge": "^3.3.1" - }, - "devDependencies": { - "@eslint/eslintrc": "^3", - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "15.5.4", - "tailwindcss": "^4", - "tw-animate-css": "^1.4.0", - "typescript": "^5" - } -} -``` +Dropdown to connect/disconnect Wallet Standard wallets via `useConnectWallet` / `useDisconnectWallet`. -## !!steps 2. Update Layout - -Next, wrap the entire Next.js application with the Solana provider. - -Update `app/layout.tsx` with the provided code. This step: - -- Imports the `SolanaProvider` component -- Wraps the application's children components with the `SolanaProvider` -- Ensures all pages and components have access to wallet functionality - - - -```tsx !! title="app/layout.tsx" -import { SolanaProvider } from "@/components/solana-provider"; -import "./globals.css"; - -export default function RootLayout({ - children -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} -``` - -```tsx !! title="app/page.tsx" -// default page -``` - -## !!steps 3. Create Wallet Connect Button - -Now build the button to connect and disconnect wallets. - -Create `components/wallet-connect-button.tsx` and add the provided code. This -dropdown button: - -- Shows available wallets when clicked -- Handles the wallet connection flow using the Wallet Standard - - - -```tsx !! title="components/wallet-connect-button.tsx" +```tsx !! title="app/components/wallet-connect-button.tsx" "use client"; import { useState } from "react"; -import { useSolana } from "@/components/solana-provider"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger -} from "@/components/ui/dropdown-menu"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { ChevronDown, Wallet, LogOut } from "lucide-react"; import { - useConnect, - useDisconnect, - type UiWallet -} from "@wallet-standard/react"; - -function truncateAddress(address: string): string { - return `${address.slice(0, 4)}...${address.slice(-4)}`; + useConnectWallet, + useDisconnectWallet, + useWallet, +} from "@solana/react-hooks"; + +const CONNECTORS = [ + { id: "wallet-standard:phantom", label: "Phantom" }, + { id: "wallet-standard:solflare", label: "Solflare" }, + { id: "wallet-standard:backpack", label: "Backpack" }, +]; + +function truncate(address: string) { + return `${address.slice(0, 4)}…${address.slice(-4)}`; } -function WalletIcon({ - wallet, - className -}: { - wallet: UiWallet; - className?: string; -}) { - return ( - - {wallet.icon && ( - - )} - {wallet.name.slice(0, 2).toUpperCase()} - - ); -} - -function WalletMenuItem({ - wallet, - onConnect -}: { - wallet: UiWallet; - onConnect: () => void; -}) { - const { setWalletAndAccount } = useSolana(); - const [isConnecting, connect] = useConnect(wallet); - - const handleConnect = async () => { - if (isConnecting) return; - +export function WalletConnectButton() { + const wallet = useWallet(); + const connectWallet = useConnectWallet(); + const disconnectWallet = useDisconnectWallet(); + const [error, setError] = useState(null); + const [open, setOpen] = useState(false); + + const isConnected = wallet.status === "connected"; + const address = isConnected + ? wallet.session.account.address.toString() + : null; + + async function handleConnect(connectorId: string) { + setError(null); try { - const accounts = await connect(); - - if (accounts && accounts.length > 0) { - const account = accounts[0]; - setWalletAndAccount(wallet, account); - onConnect(); - } + await connectWallet(connectorId, { autoConnect: true }); + setOpen(false); } catch (err) { - console.error(`Failed to connect ${wallet.name}:`, err); + setError(err instanceof Error ? err.message : "Unable to connect"); } - }; - - return ( - - ); -} + } -function DisconnectButton({ - wallet, - onDisconnect -}: { - wallet: UiWallet; - onDisconnect: () => void; -}) { - const { setWalletAndAccount } = useSolana(); - const [isDisconnecting, disconnect] = useDisconnect(wallet); - - const handleDisconnect = async () => { + async function handleDisconnect() { + setError(null); try { - await disconnect(); - setWalletAndAccount(null, null); - onDisconnect(); + await disconnectWallet(); + setOpen(false); } catch (err) { - console.error("Failed to disconnect wallet:", err); + setError(err instanceof Error ? err.message : "Unable to disconnect"); } - }; + } return ( - - - Disconnect - - ); -} - -export function WalletConnectButton() { - const { wallets, selectedWallet, selectedAccount, isConnected } = useSolana(); - - const [dropdownOpen, setDropdownOpen] = useState(false); +
+ - return ( - - - +
) : ( - <> - - Connect Wallet - - - )} - - - - - {wallets.length === 0 ? ( -

- No wallets detected -

- ) : ( - <> - {!isConnected ? ( - <> - Available Wallets - - {wallets.map((wallet, index) => ( - setDropdownOpen(false)} - /> +
+

Wallet Standard

+
+ {CONNECTORS.map((connector) => ( + ))} - - ) : ( - selectedWallet && - selectedAccount && ( - <> - Connected Wallet - -
-
- -
- - {selectedWallet.name} - - - {truncateAddress(selectedAccount.address)} - -
-
-
- - setDropdownOpen(false)} - /> - - ) - )} - - )} - - +
+
+ )} + {error ? ( +

{error}

+ ) : null} + + ) : null} + ); } ``` -```tsx !! title="app/page.tsx" -// default page -``` - -## !!steps 4. Create Send Transaction Component - -Create a component that sends a transaction invoking the memo program to add a -message to the translation logs. +## !!steps 3. Memo Sender -The purpose of this component is to demonstrate how to send transactions with -the connected wallet. +Use `useSendTransaction` to send a Memo program instruction (canonical program id +`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`). -Create `components/memo-card.tsx` and add the provided code. This component: - -- Allows users to input a message -- Creates a Solana transaction with an instruction invoking the memo program -- Requests the connected wallet to sign and send the transaction -- Displays a link to view the transaction on Solana Explorer - - - -```tsx !! title="components/memo-card.tsx" +```tsx !! title="app/components/memo-card.tsx" "use client"; +import { address } from "@solana/kit"; import { useState } from "react"; -import { useSolana } from "@/components/solana-provider"; -import { useWalletAccountTransactionSendingSigner } from "@solana/react"; -import { type UiWalletAccount } from "@wallet-standard/react"; -import { - pipe, - createTransactionMessage, - appendTransactionMessageInstruction, - setTransactionMessageFeePayerSigner, - setTransactionMessageLifetimeUsingBlockhash, - signAndSendTransactionMessageWithSigners, - getBase58Decoder, - type Signature -} from "@solana/kit"; -import { getAddMemoInstruction } from "@solana-program/memo"; - -// Component that only renders when wallet is connected -function ConnectedMemoCard({ account }: { account: UiWalletAccount }) { - const { rpc, chain } = useSolana(); - const [isLoading, setIsLoading] = useState(false); - const [memoText, setMemoText] = useState(""); - const [txSignature, setTxSignature] = useState(""); +import { useSendTransaction, useWalletSession } from "@solana/react-hooks"; - const signer = useWalletAccountTransactionSendingSigner(account, chain); +const MEMO_PROGRAM = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"; - const sendMemo = async () => { - if (!signer) return; +export function MemoCard() { + const session = useWalletSession(); + const { send, isSending, signature } = useSendTransaction(); + const [memoText, setMemoText] = useState(""); + const [error, setError] = useState(null); - setIsLoading(true); + async function sendMemo() { + if (!session) { + setError("Connect a wallet first."); + return; + } + const trimmed = memoText.trim(); + if (!trimmed) { + setError("Enter a memo message."); + return; + } + setError(null); try { - const { value: latestBlockhash } = await rpc - .getLatestBlockhash({ commitment: "confirmed" }) - .send(); - - const memoInstruction = getAddMemoInstruction({ memo: memoText }); - - const message = pipe( - createTransactionMessage({ version: 0 }), - (m) => setTransactionMessageFeePayerSigner(signer, m), - (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), - (m) => appendTransactionMessageInstruction(memoInstruction, m) - ); - - const signature = await signAndSendTransactionMessageWithSigners(message); - const signatureStr = getBase58Decoder().decode(signature) as Signature; - - setTxSignature(signatureStr); + await send({ + authority: session, + feePayer: session.account.address, + instructions: [ + { + accounts: [], + data: new TextEncoder().encode(trimmed), + programAddress: address(MEMO_PROGRAM), + }, + ], + }); setMemoText(""); - } catch (error) { - console.error("Memo failed:", error); - } finally { - setIsLoading(false); + } catch (err) { + // @ts-expect-error unknown error shape + console.error("Memo transaction failed", err?.cause ?? err); + setError(err instanceof Error ? err.message : "Failed to send memo"); } - }; + } return ( -
-
- +
+
+

Memo

+

Send a memo transaction

+

Uses the Memo program and the connected wallet as the fee payer.

+
+
+