DEV Community

Pratik S
Pratik S

Posted on

Using @solana/kit in React with Wallet Standard

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:

  1. Connects to Solana wallets using Wallet Standard
  2. Manages wallet state through React Context
  3. 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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (1)

Collapse
 
voncartergriffen profile image
Von Carter Griffen

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?