Back to Copilotkit

Render agent state in your app

showcase/shell-docs/src/content/docs/shared-state/rendering-in-app.mdx

1.60.13.8 KB
Original Source

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:

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:

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">
        <Canvas />
        <CopilotSidebar />
      </div>
    </CopilotKit>
  );
}
<Callout type="info"> `<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. </Callout>

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:

tsx
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.