branch:
StateDemo.tsx
10718 bytesRaw
import { useAgent } from "agents/react";
import { useState } from "react";
import { Button, Input, Surface, Text } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
LogPanel,
ConnectionStatus,
CodeExplanation,
HighlightedJson,
type CodeSection
} from "../../components";
import { useLogs, useUserId, useToast } from "../../hooks";
import type { StateAgent, StateAgentState } from "./state-agent";
const codeSections: CodeSection[] = [
{
title: "Define your agent with typed state",
description:
"Extend the Agent class with a state type. Set initialState and it will be automatically persisted — surviving restarts, hibernation, and reconnections.",
code: `import { Agent, callable } from "agents";
interface StateAgentState {
counter: number;
items: string[];
lastUpdated: string | null;
}
class StateAgent extends Agent<Env, StateAgentState> {
initialState: StateAgentState = {
counter: 0,
items: [],
lastUpdated: null,
};
}`
},
{
title: "Mutate state with @callable methods",
description:
"Methods decorated with @callable are exposed as RPC endpoints. Call this.setState() to update — the new state is automatically broadcast to every connected client.",
code: ` @callable()
increment(): StateAgentState {
const newState = {
...this.state,
counter: this.state.counter + 1,
lastUpdated: new Date().toISOString(),
};
this.setState(newState);
return newState;
}`
},
{
title: "Connect from React with useAgent",
description:
"The useAgent hook opens a WebSocket to your agent. The onStateUpdate callback fires whenever state changes — from this client, another client, or the server itself.",
code: `import { useAgent } from "agents/react";
const agent = useAgent({
agent: "state-agent",
name: "my-instance",
onStateUpdate: (newState, source) => {
// source is "server" or "client"
setState(newState);
},
});
// Call server methods
await agent.call("increment");
// Or set state directly from the client
agent.setState({ ...state, counter: 42 });`
}
];
export function StateDemo() {
const userId = useUserId();
const { logs, addLog, clearLogs } = useLogs();
const { toast } = useToast();
const [newItem, setNewItem] = useState("");
const [customValue, setCustomValue] = useState("0");
const [state, setState] = useState<StateAgentState>({
counter: 0,
items: [],
lastUpdated: null
});
const agent = useAgent<StateAgent, StateAgentState>({
agent: "state-agent",
name: `state-demo-${userId}`,
onStateUpdate: (newState, source) => {
addLog("in", "state_update", { source, state: newState });
if (newState) setState(newState);
},
onOpen: () => addLog("info", "connected"),
onClose: () => addLog("info", "disconnected"),
onError: () => addLog("error", "error", "Connection error")
});
const handleIncrement = async () => {
addLog("out", "call", "increment()");
try {
const result = await agent.call("increment");
addLog("in", "result", result);
toast("Counter: " + (result as StateAgentState).counter, "success");
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleDecrement = async () => {
addLog("out", "call", "decrement()");
try {
const result = await agent.call("decrement");
addLog("in", "result", result);
toast("Counter: " + (result as StateAgentState).counter, "success");
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleSetCounter = async () => {
const value = Number.parseInt(customValue, 10);
addLog("out", "call", `setCounter(${value})`);
try {
const result = await agent.call("setCounter", [value]);
addLog("in", "result", result);
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleAddItem = async () => {
if (!newItem.trim()) return;
addLog("out", "call", `addItem("${newItem}")`);
try {
const result = await agent.call("addItem", [newItem]);
addLog("in", "result", result);
setNewItem("");
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleRemoveItem = async (index: number) => {
addLog("out", "call", `removeItem(${index})`);
try {
const result = await agent.call("removeItem", [index]);
addLog("in", "result", result);
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleReset = async () => {
addLog("out", "call", "resetState()");
try {
const result = await agent.call("resetState");
addLog("in", "result", result);
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
}
};
const handleClientSetState = () => {
const value = Number.parseInt(customValue, 10);
addLog("out", "setState", { counter: value });
agent.setState({
...state,
counter: value,
lastUpdated: new Date().toISOString()
});
};
return (
<DemoWrapper
title="State Management"
description={
<>
Every agent has a{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
state
</code>{" "}
object that is automatically persisted and synchronized. When you call{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
this.setState()
</code>{" "}
on the server, every connected client receives the update instantly
over WebSocket. Clients can also set state directly — changes flow
both ways. State survives restarts, hibernation, and reconnections.
Try incrementing the counter, then refresh the page.
</>
}
statusIndicator={
<ConnectionStatus
status={
agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
}
/>
}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Controls */}
<div className="space-y-6">
{/* Counter Controls */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Counter: {state.counter}</Text>
</div>
<div className="flex gap-2 mb-4">
<Button variant="secondary" onClick={handleDecrement}>
-1
</Button>
<Button variant="primary" onClick={handleIncrement}>
+1
</Button>
</div>
<Input
aria-label="Custom counter value"
type="number"
value={customValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCustomValue(e.target.value)
}
className="w-full mb-2"
placeholder="Custom value"
/>
<div className="flex gap-2">
<Button
variant="secondary"
onClick={handleSetCounter}
className="flex-1"
>
Set (Server)
</Button>
<Button
variant="secondary"
onClick={handleClientSetState}
className="flex-1"
>
Set (Client)
</Button>
</div>
</Surface>
{/* Items List */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Items ({state.items.length})</Text>
</div>
<div className="flex gap-2 mb-4">
<Input
aria-label="New item"
type="text"
value={newItem}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setNewItem(e.target.value)
}
onKeyDown={(e: React.KeyboardEvent) =>
e.key === "Enter" && handleAddItem()
}
className="flex-1"
placeholder="New item"
/>
<Button variant="primary" onClick={handleAddItem}>
Add
</Button>
</div>
{state.items.length > 0 ? (
<ul className="space-y-1">
{state.items.map((item: string, i: number) => (
<li
key={i}
className="flex items-center justify-between py-1 px-2 bg-kumo-elevated rounded"
>
<span className="text-sm text-kumo-default">{item}</span>
<Button
variant="ghost"
size="xs"
onClick={() => handleRemoveItem(i)}
className="text-kumo-danger"
>
Remove
</Button>
</li>
))}
</ul>
) : (
<p className="text-sm text-kumo-inactive">No items</p>
)}
</Surface>
{/* State Display */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="flex items-center justify-between mb-2">
<Text variant="heading3">Current State</Text>
<Button variant="destructive" size="xs" onClick={handleReset}>
Reset
</Button>
</div>
<HighlightedJson data={state} />
{state.lastUpdated && (
<p className="text-xs text-kumo-inactive mt-2">
Last updated: {new Date(state.lastUpdated).toLocaleString()}
</p>
)}
</Surface>
</div>
{/* Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={clearLogs} maxHeight="400px" />
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}