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:

components/Canvas.tsx
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:

app/page.tsx
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.state as possibly partial while a run is in progress. Guard with defaults (as above) so half-streamed state doesn't crash your render.