Render agent state in your app
Read agent.state with useAgent and render it in your main view or canvas.
Shared state is most powerful when the agent's state shows up in
your application UI, such as a dashboard, document canvas, map, or table. Because
agent.state is plain React data, you can subscribe to it from any component in
your tree and render it however you like.
The pattern#
useAgent works in any component under <CopilotKit>. It doesn't have to be
near the chat. Call it in your main-view component, read agent.state, and render:
import { useAgent } from "@copilotkit/react-core/v2";
type CanvasState = {
title: string;
items: { id: string; label: string; done: boolean }[];
};
export function Canvas() {
// No agentId means the "default" agent. Pass { agentId } to target another.
const { agent } = useAgent();
const state = (agent.state ?? {}) as Partial<CanvasState>;
return (
<main className="canvas">
<h1>{state.title ?? "Untitled"}</h1>
<ul>
{(state.items ?? []).map((item) => (
<li key={item.id} data-done={item.done}>
{item.label}
</li>
))}
</ul>
</main>
);
}Every time the agent mutates its state, whether from a tool call, node
transition, or streamed update, useAgent re-renders this component
with the new values. The chat can be in a sidebar, a popup, or absent entirely;
your canvas updates the same way.
Put it anywhere in your layout#
The agent lives on the <CopilotKit> provider, so the chat surface and your
main-view components are just two consumers of the same agent. A typical layout
renders the canvas as the primary content and the chat as a docked sidebar:
import { CopilotKit, CopilotSidebar } from "@copilotkit/react-core/v2";
import { Canvas } from "../components/Canvas";
export default function Page() {
return (
<CopilotKit runtimeUrl="/api/copilotkit">
<div className="app-shell">
{/* Your app UI, driven by agent.state */}
<Canvas />
{/* Chat is just another consumer of the same agent */}
<CopilotSidebar />
</div>
</CopilotKit>
);
}<Canvas> and <CopilotSidebar> both call useAgent() for the same agentId,
so they share one agent instance and one state object. There's nothing chat-specific
about reading agent.state. The sidebar is not special.
about reading agent.state. The sidebar is not special.
Writing back from the main view#
The same agent exposes setState, so your canvas can be interactive, not just a
read-only mirror. A click handler in the main view can push a new value that the
agent reads on its next turn:
function toggleItem(id: string) {
agent.setState({
...agent.state,
items: (agent.state?.items ?? []).map((it) =>
it.id === id ? { ...it, done: !it.done } : it,
),
});
}This is the same two-way channel described in Shared State. The only difference here is that the reads and writes happen in your application's main surface rather than in the chat.
Tips#
- Target a specific agent with
useAgent({ agentId: "research-agent" })when you have more than one. The default is the agent named"default". - Throttle high-frequency updates with
useAgent({ throttleMs })if a streaming run re-renders a heavy canvas too often. - Treat
agent.stateas possibly partial while a run is in progress. Guard with defaults (as above) so half-streamed state doesn't crash your render.
Related#
- Shared State: the full read/write model and the underlying
useAgentsubscription. - State streaming: stream a tool argument into a state key so the canvas fills in token-by-token.
- Agent read-only context: push UI values to the agent without letting it write back.
useAgentreference: full hook signature and options.