Week 4 • Anchor Client With Kit
Anchor Client Patterns with Kit
Connect Anchor-style program actions to a kit-first client service layer with explicit responsibilities.
Step 1: 0-to-1 Theory
Primitives
program action service: one place where program-specific calls liveui action: button/form that calls servicetransport: send/confirm pipeline shared across app
Mental Model
Think of an on-chain program as an API server that only understands:
- instruction bytes
- account lists (read/write/sign)
Your client is responsible for building the instruction call, gathering accounts, then using the same send/confirm pipeline every time.
Invariants
- UI does not own program call internals.
- Program service returns normalized app-level results.
- Transaction lifecycle remains shared and reusable.
Quick Checks
- Why is "service layer first" important for scaling dApps?
- Why avoid raw program errors in UI?
Step 2: Real-World Implementation (Code Solution)
Create lib/solana/programs/counterService.ts:
// Normalize results so UI logic stays simple and predictable.
export type ActionResult =
| { ok: true; signature: string }
| { ok: false; error: string };
export async function incrementCounterAction(): Promise<ActionResult> {
try {
// In your full app, this calls your transaction helper + program instruction builder.
// Keep this file as the only place where counter-program action logic is defined.
// For now, return a fake signature to demonstrate the pattern.
const fakeSignature = "5kK...demo-signature";
return { ok: true, signature: fakeSignature };
} catch (e: any) {
// Always stringify unknown error shapes before returning to UI.
return { ok: false, error: String(e?.message ?? e) };
}
}Create components/solana/CounterActionButton.tsx:
"use client";
import React, { useState } from "react";
import { incrementCounterAction } from "@/lib/solana/programs/counterService";
export function CounterActionButton() {
// Explicit states make it harder to accidentally double-submit.
const [state, setState] = useState<"idle" | "working" | "ok" | "error">("idle");
const [message, setMessage] = useState("");
const onClick = async () => {
setState("working");
const result = await incrementCounterAction();
if (result.ok) {
setState("ok");
// Always show signature on success so users can verify in an explorer.
setMessage(`Signature: ${result.signature}`);
} else {
setState("error");
setMessage(result.error);
}
};
return (
<div className="space-y-2">
<button onClick={onClick} disabled={state === "working"} className="px-3 py-2 rounded bg-emerald-700 text-white">
{state === "working" ? "Submitting..." : "Increment Counter"}
</button>
{message && <p className="text-xs text-zinc-300">{message}</p>}
</div>
);
}Expected result:
- program action logic is isolated and UI stays simple.
Step 3: Mastery Test
- Easy: what does normalized return shape solve?
- Medium: where should retries live: UI or service layer?
- Hard: how would you test service logic without rendering components?