# Generative UI
URL: /docs/runtimes/langgraph/generative-ui
Render structured UI components emitted by LangGraph alongside assistant messages.
LangGraph's [Generative UI](https://docs.langchain.com/langsmith/generative-ui-react) lets your graph emit structured UI components alongside assistant messages via `push_ui_message` (Python) or `typedUi().push()` (TypeScript). The assistant-ui adapter translates these into [`DataMessagePart`s](/docs/guides/tool-ui) on the associated assistant message, which you render with the existing `makeAssistantDataUI` API.
## Enable the `custom` stream mode \[#enable-the-custom-stream-mode]
UI messages are emitted through LangGraph's `custom` stream channel. Make sure your `sendMessage` helper includes `"custom"` in `streamMode`:
```ts
streamMode: ["messages", "updates", "custom"];
```
If your graph accumulates UI messages in state under the `ui` key (the default for `typedUi`), `"values"` also works; the adapter reads both paths.
## Custom state key \[#custom-state-key]
If your graph uses a non-default `stateKey` with `typedUi(config, { stateKey: "my_ui" })` on the server, pass the matching `uiStateKey` option to `useLangGraphRuntime`:
```ts
const runtime = useLangGraphRuntime({
stream: async function* (messages, { initialize }) {
/* ... */
},
uiStateKey: "my_ui",
});
```
This only affects the `values` stream path; the `custom` channel carries each UI event individually and does not rely on the state key.
## Emit a UI message from your graph \[#emit-a-ui-message-from-your-graph]
```python
from langgraph.graph.ui import push_ui_message
from langchain_core.messages import AIMessage
async def chart_node(state, config):
message = AIMessage(id="msg-1", content="Here's your chart.")
push_ui_message(
"chart",
{"series": [1, 2, 3], "title": "Sales"},
message=message, # Links the UI to this AI message
)
return {"messages": [message]}
```
```ts
import { typedUi } from "@langchain/langgraph-sdk/react-ui/server";
import type { ComponentRegistry } from "./components";
export async function chartNode(state, config) {
const ui = typedUi(config);
const message = { id: "msg-1", type: "ai", content: "Here's your chart." };
ui.push(
{ name: "chart", props: { series: [1, 2, 3], title: "Sales" } },
{ message },
);
return { messages: [message] };
}
```
Passing `message` (Python) or `{ message }` (TypeScript) is what links the UI component to a specific assistant message. The adapter reads `metadata.message_id` to attach the generated `DataMessagePart` to the correct message in the thread.
## Register a renderer on the client \[#register-a-renderer-on-the-client]
```tsx title="@/components/ChartUI.tsx"
import { makeAssistantDataUI } from "@assistant-ui/react";
type ChartProps = {
series: number[];
title: string;
};
export const ChartUI = makeAssistantDataUI({
name: "chart",
render: ({ data }) => (
{data.title}
),
});
```
Mount the component once somewhere inside the `AssistantRuntimeProvider` tree. It renders nothing itself; it only registers the renderer:
```tsx title="@/components/MyAssistant.tsx"
```
When a matching UI message arrives, the adapter appends a `{ type: "data", name: "chart", data: { series, title } }` part to the parent assistant message and the registered component renders inline.
## Register renderers via `uiComponents` \[#register-renderers-via-uicomponents]
Instead of mounting separate `makeAssistantDataUI` components, register renderers directly on the runtime hook:
```tsx
const runtime = useLangGraphRuntime({
stream: async function* (messages, { initialize }) {
/* ... */
},
uiComponents: {
renderers: {
chart: ({ data }) => ,
table: ({ data }) => ,
},
},
});
```
Static `renderers` are matched by `ui_message` name. If no match is found, the part renders nothing unless a `fallback` is provided.
## Dynamic loading with `fallback` \[#dynamic-loading-with-fallback]
LangSmith's [Generative UI](https://docs.langchain.com/langsmith/generative-ui-react) supports colocating UI code with your graph and loading it at runtime via `LoadExternalComponent`. The `fallback` option handles any `ui_message` name that has no static renderer:
```tsx
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
const runtime = useLangGraphRuntime({
stream: async function* (messages, { initialize }) {
/* ... */
},
uiComponents: {
fallback: ({ name, data }) => (
),
renderers: {
chart: ({ data }) => ,
},
},
});
```
With this setup:
* A `ui_message` with `name: "chart"` renders the static `Chart` component.
* Any other name (e.g. `"dashboard"`, `"form"`) is handled by `fallback`, which fetches the component from LangSmith at runtime.
`fallback` receives the same props as any data renderer: `name`, `data`, and part state metadata.
## Semantics \[#semantics]
The adapter mirrors the reducer in `@langchain/langgraph-sdk/react-ui` exactly:
* UI messages are keyed by their own `id`. Pushing the same id again **replaces** the existing entry.
* Passing `metadata: { merge: true }` shallow-merges `props` onto the previous entry.
* Emitting `{ type: "remove-ui", id }` (via `delete_ui_message` or `ui.delete(id)`) removes the entry.
* UI messages without `metadata.message_id` are held in the runtime but not injected into any message; use `useLangGraphUIMessages()` to access the raw list if needed.
## Restore persisted UI messages on thread switch \[#restore-persisted-ui-messages-on-thread-switch]
If your graph persists UI messages in state via `typedUi`, return them from the `load` callback so they are restored when the user switches threads or refreshes the page:
```tsx
const runtime = useLangGraphRuntime({
stream: async function* (messages, { initialize }) {
/* ... */
},
load: async (externalId) => {
const state = await getThreadState(externalId);
return {
messages: state.values.messages,
uiMessages: state.values.ui,
interrupts: state.tasks[0]?.interrupts,
};
},
});
```
Without this, each reload starts with an empty UI list even though the messages themselves are loaded.
## Escape hatch: `useLangGraphUIMessages` \[#escape-hatch-uselanggraphuimessages]
```tsx
import { useLangGraphUIMessages } from "@assistant-ui/react-langgraph";
function Sidebar() {
const uiMessages = useLangGraphUIMessages();
// Filter, group, or render UI messages outside the thread
return <>{uiMessages.map(/* ... */)}>;
}
```
## Related \[#related]