This guide demonstrates how to integrate @solana/kit with React applications using Wallet Standard. While there are many resources for @solana/web3.js, comprehensive guides for @solana/kit are limited.
Prerequisites: Familiarity with Solana development. If you're new to SolanaKit, review the official documentation first.
you can find the full react app code here
Overview
We'll build a React application that:
- Connects to Solana wallets using Wallet Standard
- Manages wallet state through React Context
- Signs and sends transactions using @solana/kit
Setup
Install the required dependencies in your existing React application:
npm install @solana/kit @wallet-standard/react @solana/react
1. Wallet Context Setup
Create a React context to share wallet state across your application. This approach uses types from @wallet-standard/react
to build a custom context.
import { createContext, useContext, useState } from "react";
import type { ReactNode } from "react";
import type { UiWalletAccount, UiWallet } from "@wallet-standard/react";
interface ConnectedWallet {
account: UiWalletAccount;
wallet: UiWallet;
}
interface WalletContextType {
account: UiWalletAccount | null;
wallet: UiWallet | null;
connectedWallet: ConnectedWallet | null;
setConnectedWallet: (wallet: ConnectedWallet | null) => void;
isConnected: boolean;
}
const WalletContext = createContext<WalletContextType | undefined>(undefined);
export function WalletProvider({ children }: { children: ReactNode }) {
const [connectedWallet, setConnectedWallet] =
useState<ConnectedWallet | null>(null);
const value = {
account: connectedWallet?.account || null,
wallet: connectedWallet?.wallet || null,
connectedWallet,
setConnectedWallet,
isConnected: !!connectedWallet,
};
return (
<WalletContext.Provider value={value}>{children}</WalletContext.Provider>
);
}
export function useWallet() {
const context = useContext(WalletContext);
if (context === undefined) {
throw new Error("useWallet must be used within a WalletProvider");
}
return context;
}
2. Wallet Connect Modal
Create a modal component that displays available wallets for connection. We use the useWallets
hook to get all available wallets and filter for Solana chain ones.
import { useWallet } from "../../contexts/WalletContext";
import { useWallets } from "@wallet-standard/react";
import { WalletModal } from "./WalletModal";
export function ConnectWalletBtn() {
const [isModalOpen, setIsModalOpen] = useState(false);
const wallets = useWallets();
const { isConnected } = useWallet();
const solanaWallets = wallets.filter((wallet) =>
wallet.chains.some((chain) => chain.startsWith("solana:"))
);
if (isConnected) {
return null; // Hide connect button when already connected
}
return (
<>
<Button onClick={() => setIsModalOpen(true)}>Connect Wallet</Button>
<WalletListModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
wallets={solanaWallets}
/>
</>
);
}
3. Wallet Modal Implementation
The modal displays available wallet options. Each wallet item uses useConnect
and useDisconnect
hooks from @wallet-standard/react
to handle wallet operations.
import { useWallet } from "@/contexts/WalletContext";
import {
useConnect,
useDisconnect,
type UiWallet,
} from "@wallet-standard/react";
export function WalletListModal({
isOpen,
onClose,
wallets,
}: WalletListModalProps) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Connect Wallet</DialogTitle>
</DialogHeader>
<DialogDescription>Select a wallet to connect to.</DialogDescription>
<div className="flex flex-col gap-2">
{wallets.map((wallet) => (
<WalletListItem
key={wallet.name}
wallet={wallet}
onConnect={onClose}
/>
))}
</div>
</DialogContent>
</Dialog>
);
}
4. Individual Wallet Items
Each wallet item handles connection and disconnection logic. The handleConnect
function awaits the connection, then updates the context with the connected account and wallet information.
export const WalletListItem = ({ wallet, onConnect }: WalletItemProps) => {
const [isConnecting, connect] = useConnect(wallet);
const [isDisconnecting, disconnect] = useDisconnect(wallet);
const { setConnectedWallet, isConnected } = useWallet();
useEffect(() => {
if (isDisconnecting) {
setConnectedWallet(null);
}
}, [isDisconnecting, setConnectedWallet]);
const handleConnect = async () => {
try {
const connectedAccount = await connect();
if (!connectedAccount.length) {
console.warn(`Connect to ${wallet.name} but there are no accounts.`);
return connectedAccount;
}
const first = connectedAccount[0];
setConnectedWallet({ account: first, wallet });
onConnect?.(); // Close modal after successful connection
return connectedAccount;
} catch (error) {
console.error("Failed to connect wallet:", error);
}
};
return (
<Button
variant="outline"
className="w-full justify-start p-4 h-auto border-2 hover:border-primary/50 hover:bg-accent/50 transition-all duration-200"
onClick={isConnected ? disconnect : handleConnect}
disabled={isConnecting}
>
<div className="flex items-center space-x-3 w-full">
{wallet.icon ? (
<img
src={wallet.icon}
alt={wallet.name}
className="w-10 h-10 rounded-lg object-cover border border-border"
/>
) : (
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center border border-border">
<span className="text-muted-foreground text-lg font-semibold">
{wallet.name.charAt(0).toUpperCase()}
</span>
</div>
)}
<div className="flex-1 text-left">
<div className="font-semibold text-foreground">
{isConnecting ? "Connecting..." : wallet.name}
</div>
<div className="text-sm text-muted-foreground">
{isConnecting ? "Please wait..." : "Click to connect"}
</div>
</div>
{isConnecting && (
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
)}
</div>
</Button>
);
};
5. Transaction Signing with @solana/kit
The core of @solana/kit integration lies in transaction signing. We use the useWalletAccountTransactionSendingSigner
hook to obtain a signer for the connected wallet, which enables us to create, sign, and send transactions directly through the wallet.
The transaction flow begins by creating a transaction message using createTransactionMessage
. We then use the pipe function to chain operations: setting the fee payer signer, configuring the transaction lifetime with a blockhash, and appending the necessary instructions. Finally, we use signAndSendTransactionMessageWithSigners
to have the wallet sign and send the transaction, returning the signature bytes.
Here's the complete implementation showing how to create a memo transaction:
import { useState } from "react";
import { useWalletAccountTransactionSendingSigner } from "@solana/react";
import {
appendTransactionMessageInstruction,
createSolanaRpc,
getBase58Decoder,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signAndSendTransactionMessageWithSigners,
createTransactionMessage,
} from "@solana/kit";
import { useWallet } from "../contexts/WalletContext";
import { getAddMemoInstruction } from "@solana-program/memo";
interface MemoTransactionButtonProps {
text: string;
rpcUrl?: string;
}
export function MemoTransactionButton({
text,
rpcUrl = "https://api.devnet.solana.com",
}: MemoTransactionButtonProps) {
const { account } = useWallet();
const [isLoading, setIsLoading] = useState(false);
if (!account) {
return null;
}
const signer = useWalletAccountTransactionSendingSigner(
account,
"solana:devnet"
);
const handleRecordMemo = async () => {
setIsLoading(true);
try {
const { value: latestBlockhash } = await createSolanaRpc(rpcUrl)
.getLatestBlockhash()
.send();
const message = pipe(
createTransactionMessage({ version: "legacy" }),
(m) => setTransactionMessageFeePayerSigner(signer, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
(m) =>
appendTransactionMessageInstruction(
getAddMemoInstruction({ memo: text }),
m
)
);
const signatureBytes = await signAndSendTransactionMessageWithSigners(
message
);
const base58Signature = getBase58Decoder().decode(signatureBytes);
window.alert(
`Transaction sent! View on Solana Explorer: https://explorer.solana.com/tx/${base58Signature}?cluster=devnet`
);
} catch (e) {
console.error("Failed to record memo", e);
window.alert("Failed to record memo: " + (e as Error).message);
} finally {
setIsLoading(false);
}
};
return (
<button
className="memo-transaction-button"
onClick={handleRecordMemo}
disabled={isLoading}
>
{isLoading ? (
<>
<span className="loading-spinner">⏳</span>
Signing Transaction...
</>
) : (
`Record Memo: ${text}`
)}
</button>
);
}
This is a rough guide to get you started with using @solana/kit in a react app with wallet standard. I wasn't able to find any so i created one hope you find it useful.
Additional Resources
For more advanced implementations and examples, explore:
- wallet-ui - Comprehensive wallet UI components
- grill wallet-adapter-compat - Compatibility layer for existing wallet adapters
Top comments (1)
Appreciate the research and clear examples—this fills a real gap for @solana/kit + Wallet Standard. Quick question: how do you handle network switching (devnet/mainnet) while keeping the signer in sync—do you manage chain state in the WalletProvider and recreate the signer on change, or derive it per component?