learn.sol
Solana Kit Clients • Anchor Client With Kit
Lesson 8 of 11

Anchor Client Patterns with Kit

Build one real program action flow by keeping instruction building in a service layer and transport in the shared client pipeline.

The main client-side mistake with Anchor is simple.

People let React components assemble raw program calls directly.

That feels fast on day one.

It becomes a mess as soon as the app has two buttons, three account types, and one real failure path.

The correct model is:

  • the service layer knows how to build program instructions
  • the shared client transport knows how to send them
  • the UI only gathers input, triggers the action, and renders status

That split is what keeps a Solana app explainable.

Service LayerTap to reveal
The client code that knows how to assemble program instructions and account inputs without being coupled to React rendering.
Service Layer
Instruction BuilderTap to reveal
The generated or hand-written function that returns the concrete program instruction for one action.
Instruction Builder
Shared TransportTap to reveal
The wallet-aware send pipeline that submits instructions without letting each button reinvent transaction delivery.
Shared Transport
UI BoundaryTap to reveal
The narrow component responsibility of gathering input, triggering the action, and rendering status instead of understanding program wiring.
UI Boundary

What This Lesson Owns

This lesson is not about generating an SDK.

It is about using one correctly.

Assume your Anchor program already exposes a generated instruction builder for the increment instruction.

That builder can come from your IDL generation step.

The important point is where you use it.

Step 1: Build The Program Action In One Place

Create lib/solana/programs/counterService.ts:

import { address, type Address } from "@solana/kit";
import { getIncrementInstruction } from "@/lib/generated/counter";

export type BuildIncrementCounterInput = {
  counterAddress: string;
  authorityAddress: string;
};

export function buildIncrementCounterAction(input: BuildIncrementCounterInput) {
  const counter = address(input.counterAddress) as Address;
  const authority = address(input.authorityAddress) as Address;

  return {
    instructions: [
      getIncrementInstruction({
        counter,
        authority,
      }),
    ],
  };
}

What this file is doing:

  • converting loose UI strings into typed addresses
  • calling the generated instruction builder
  • returning a transport-ready instruction list

What it is not doing:

  • rendering UI
  • opening wallet prompts
  • deciding button state

That boundary matters.

If instruction assembly lives inside the component, every future program action will duplicate logic badly.

Step 2: Send Through The Shared Client Transport

Now create components/solana/CounterActionButton.tsx:

"use client";

import React, { useState } from "react";
import { useSendTransaction, useWallet } from "@solana/react-hooks";
import { buildIncrementCounterAction } from "@/lib/solana/programs/counterService";

type CounterActionButtonProps = {
  counterAddress: string;
};

export function CounterActionButton({ counterAddress }: CounterActionButtonProps) {
  const wallet = useWallet();
  const { send, isSending, signature, error } = useSendTransaction();
  const [message, setMessage] = useState("");

  const onClick = async () => {
    setMessage("");

    if (wallet.status !== "connected") {
      setMessage("Connect a wallet before submitting a program action.");
      return;
    }

    try {
      const action = buildIncrementCounterAction({
        counterAddress,
        authorityAddress: wallet.session.account.address,
      });

      await send({ instructions: action.instructions });
      setMessage("Counter increment submitted successfully.");
    } catch (e: any) {
      setMessage(String(e?.message ?? e));
    }
  };

  return (
    <div className="space-y-2">
      <button
        onClick={onClick}
        disabled={wallet.status !== "connected" || isSending}
        className="px-3 py-2 rounded bg-emerald-700 text-white"
      >
        {isSending ? "Submitting..." : "Increment Counter"}
      </button>

      {message ? <p className="text-xs text-zinc-300">{message}</p> : null}
      {signature ? <p className="text-xs text-zinc-300">Signature: {signature}</p> : null}
      {error ? <p className="text-xs text-red-300">{String(error)}</p> : null}
    </div>
  );
}

Read the responsibility split carefully:

  • buildIncrementCounterAction(...) knows the program accounts and instruction shape
  • useSendTransaction() knows how to push those instructions through the wallet-aware client transport
  • the component knows when to disable itself and what to show the user

That is the architecture you want.

Why This Pattern Scales

When the next instruction arrives, you do not rewrite the whole stack.

You add another service function.

Examples:

  • buildCreateProfileAction(...)
  • buildCloseEscrowAction(...)
  • buildClaimRewardsAction(...)

The transport stays shared.

The UI state pattern stays shared.

Only the program-specific instruction builder changes.

That is how client code stops collapsing into copy-paste logic.

The Failure Mode To Avoid

Do not do this inside a component:

  • parse addresses
  • build instruction accounts
  • compute PDA inputs
  • choose instruction builder
  • send transaction
  • map raw errors

That is too much responsibility for one button.

You will lose testability first.

Then you will lose correctness.

What You Can Do After This

You can keep Anchor program specifics in one service layer while still using a kit-first transport and hook-based wallet flow.

That is the bridge between on-chain program logic and a maintainable client.

The next lesson builds on that by making transaction status and recovery behavior explicit in the UI.

Quick Check

Quick Check
Single answer

Why should the client build Anchor instructions in a service layer instead of directly inside the button component?

Quick Check
Single answer

What should stay shared when the app adds more Anchor actions?

Sources

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px