showcase/shell-docs/src/content/docs/shared-state/rendering-in-app.mdx
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.
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.
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">
<Canvas />
<CopilotSidebar />
</div>
</CopilotKit>
);
}
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.
useAgent({ agentId: "research-agent" }) when
you have more than one. The default is the agent named "default".useAgent({ throttleMs }) if a
streaming run re-renders a heavy canvas too often.agent.state as possibly partial while a run is in progress. Guard
with defaults (as above) so half-streamed state doesn't crash your render.useAgent subscription.useAgent reference: full hook signature and options.