LangChain React Runtime

Use LangChain's useStream hook with a React chat UI through assistant-ui — a lighter LangGraph adapter that delegates streaming to @langchain/react.

@assistant-ui/react-langchain wraps useStream from @langchain/react and exposes it as an assistant-ui runtime. It targets the same backend as @assistant-ui/react-langgraph (LangGraph Cloud) but at a higher level, delegating stream plumbing to the upstream hook.

When to use it

Pick react-langchain over react-langgraph when:

  • You are scaffolding via npx create-assistant-ui -t langchain (the official template uses it).
  • Your app already depends on @langchain/react and uses useStream elsewhere.
  • You want to read custom state keys (todos, files, plans) reactively with useLangChainState<T>(key).
  • You prefer a thin wrapper that stays pinned to upstream behavior.

Pick react-langgraph instead when:

  • You need a custom backend stream (your own async generator) rather than useStream's transport, or you're maintaining an existing react-langgraph app.

The comparison below lists what each adapter supports. react-langchain is the newer, thinner wrapper pinned to upstream useStream.

Architecture

@assistant-ui/react-langchain is layered on ExternalStoreRuntime (see architecture). Graph state is the source of truth; the runtime renders messages from state.values.messages and submits user input back to the graph.

Shared adapters (attachments, speech, dictation, voice, feedback) work the same way described in adapters. Cloud thread persistence is built in.

thread.isLoading reflects useStream's isThreadLoading (initial history hydration), separate from isRunning (a run being in-flight).

Requirements

  • A LangGraph Cloud API server (locally via LangGraph Studio or hosted via LangSmith).
  • The graph state must include a messages key with LangChain-alike messages, or pass a custom messagesKey.

Quickstart

Install dependencies

npm install @assistant-ui/react @assistant-ui/react-langchain @langchain/react @langchain/langgraph-sdk

Define the assistant component

@/components/MyAssistant.tsx
"use client";

import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useStreamRuntime } from "@assistant-ui/react-langchain";

export function MyAssistant() {
  const runtime = useStreamRuntime({
    assistantId: process.env["NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID"]!,
    apiUrl: process.env["NEXT_PUBLIC_LANGGRAPH_API_URL"],
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <Thread />
    </AssistantRuntimeProvider>
  );
}

Mount the component

@/app/page.tsx
import { MyAssistant } from "@/components/MyAssistant";

export default function Home() {
  return (
    <main className="h-dvh">
      <MyAssistant />
    </main>
  );
}

Set environment variables

.env.local
NEXT_PUBLIC_LANGGRAPH_API_URL=http://localhost:2024
NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID=your_graph_id

Set up UI components

useStreamRuntime options

useStreamRuntime accepts every option upstream useStream does, plus these assistant-ui-specific fields:

OptionTypeDescription
cloudAssistantCloudOptional. Uses Assistant Cloud for the thread list and a copy of settled messages for the dashboard.
create() => Promise<{ externalId: string | undefined }>Optional. Creates a thread in your backend.
delete(threadId: string) => Promise<void>Optional. When Cloud owns the thread list, called before a thread is deleted and given its Cloud thread id; read that thread's external_id to delete the backend thread, as LangGraph shows.
onThreadIdChange(threadId: string | undefined) => voidOptional. Called when the active thread's canonical ID changes.
unstable_threadListAdapterRemoteThreadListAdapterOptional. Replaces the Cloud thread list adapter.
adapters{ attachments?, speech?, dictation?, voice?, feedback? }Optional. Attachment, speech, dictation, voice, and feedback adapters.
autoCancelPendingToolCallsbooleanOptional. When the user sends a message while tool calls are still pending, submits tool messages that cancel them. Defaults to true.
unstable_allowCancellationbooleanOptional. Routes the Cancel button to useStream().stop(). On by default; pass false to disable the Cancel button.
messagesKeystringThe state key that holds messages. Defaults to "messages".
uiStateKeystringThe state key that holds generative UIMessages. Defaults to "ui".

Forwarding per-run config

When a message is sent, its runConfig.custom (for example a selected mode or model) is forwarded on the underlying useStream().submit call as config.configurable. Automatic tool-result resumes and the interrupt helpers (useLangChainRespond, useLangChainRespondAll, and useLangChainSubmit(null, { command })) reuse that same recorded configurable unless the caller passes config. Raw useLangChainSubmit / useLangChainSend calls that start a new run do not inherit it. The recording is session-scoped and does not survive a reload. Read it in the graph from config["configurable"]; on LangGraph v1 the same values are also reachable through the Runtime context, which config.configurable is aliased to.

Reading custom state keys

LangGraph agents often expose structured state beyond messages (plans, todos, scratch files, generative-UI artifacts). Read them directly with useLangChainState. It mirrors useStream().values[key] upstream and updates when the stream emits new state.

import { useLangChainState } from "@assistant-ui/react-langchain";

type Todo = { id: string; title: string; done: boolean };

function TodoList() {
  const todos = useLangChainState<Todo[]>("todos", []);
  return (
    <ul>
      {todos.map((t) => (
        <li key={t.id}>
          {t.done ? "✓" : "○"} {t.title}
        </li>
      ))}
    </ul>
  );
}

Signatures:

useLangChainState<T>(key: string): T | undefined;
useLangChainState<T>(key: string, defaultValue: T): T;

This is useful with the deepagents middleware, whose write_todos step updates state.todos alongside the tool-call stream. Reading the state key directly avoids reconstructing the list from partial tool-call args.

Interrupts

LangGraph interrupts pause the graph and wait for client input. useLangChainInterruptState exposes the current interrupt; useLangChainRespond resumes it with a response payload, while useLangChainSubmit resumes the graph with a raw state update.

Prefer useLangChainRespond: it carries the response payload via useStream().respond and handles interrupt namespaces, so you don't have to construct a Command.

import {
  useLangChainInterruptState,
  useLangChainRespond,
} from "@assistant-ui/react-langchain";

function InterruptPrompt() {
  const interrupt = useLangChainInterruptState();
  const respond = useLangChainRespond();
  if (!interrupt) return null;
  return (
    <div>
      <pre>{JSON.stringify(interrupt.value, null, 2)}</pre>
      <button onClick={() => respond({ approved: true })}>Approve</button>
    </div>
  );
}

When several interrupts are pending at once (e.g. parallel approvals), use useLangChainRespondAll() to resume them all in one run. Sequential useLangChainRespond calls can't, since the first resume starts a run and strands the rest.

import {
  useLangChainInterrupts,
  useLangChainRespondAll,
} from "@assistant-ui/react-langchain";

const interrupts = useLangChainInterrupts();
const respondAll = useLangChainRespondAll();
// resume every pending interrupt with the same payload:
await respondAll(
  Object.fromEntries(
    interrupts.flatMap((i) => (i.id ? [[i.id, { approved: true }]] : [])),
  ),
);

You can also resume with a raw Command via useLangChainSubmit:

import {
  useLangChainInterruptState,
  useLangChainSubmit,
} from "@assistant-ui/react-langchain";

function InterruptPrompt() {
  const interrupt = useLangChainInterruptState();
  const submit = useLangChainSubmit();
  if (!interrupt) return null;
  return (
    <div>
      <pre>{JSON.stringify(interrupt.value, null, 2)}</pre>
      <button
        onClick={() =>
          submit(null, { command: { resume: "approved" } })
        }
      >
        Approve
      </button>
    </div>
  );
}

Tool calls

useLangChainToolCalls exposes the root tool calls useStream assembles from the tools channel. Each entry carries a name, args, and id. Use it to render pending or streamed tool calls and approval UIs. It defaults to an empty array, so you can .map without a guard.

import { useLangChainToolCalls } from "@assistant-ui/react-langchain";

function PendingToolCalls() {
  const toolCalls = useLangChainToolCalls();
  return (
    <ul>
      {toolCalls.map((tc) => (
        <li key={tc.id}>
          {tc.name}: <code>{JSON.stringify(tc.args)}</code>
        </li>
      ))}
    </ul>
  );
}

Subagent and subgraph discovery

For multi-agent graphs, useStream tracks which subagents and subgraphs it has seen on the current run as cheap discovery maps. useLangChainSubagents and useLangChainSubgraphs surface those maps, keyed by namespace, with each value a SubagentDiscoverySnapshot / SubgraphDiscoverySnapshot. Both default to a stable empty map, so you can iterate without a guard.

These replace react-langgraph's subgraph eventHandlers: v1 surfaces discovery as maps you read reactively rather than per-event callbacks.

import {
  useLangChainSubagents,
  useLangChainSubgraphs,
} from "@assistant-ui/react-langchain";

function Discovered() {
  const subagents = useLangChainSubagents();
  const subgraphs = useLangChainSubgraphs();
  return (
    <ul>
      {[...subagents.keys()].map((ns) => (
        <li key={ns}>subagent: {ns}</li>
      ))}
      {[...subgraphs.keys()].map((ns) => (
        <li key={ns}>subgraph: {ns}</li>
      ))}
    </ul>
  );
}

Subagent and subgraph views

Every task subagent transcript is attached to its spawning tool call as ToolCallMessagePart.messages, keyed by the task tool call id. It streams while the subagent runs and nests recursively up to sixteen levels deep when subagents spawn subagents. MessagePartPrimitive.Messages renders the transcript and s.thread.tasks lists it. See Multi-agent tools. Threads loaded from history carry these transcripts because the SDK seeds stream.subagents from checkpoint messages and seeds the scoped message projections from history.

The discovery maps above pair with v1's scoped selector hooks to build a custom subagent or subgraph view. useLangChainStream exposes the underlying useStream handle; feed it to useMessages(stream, target) / useToolCalls(stream, target) from @langchain/react, with a SubagentDiscoverySnapshot / SubgraphDiscoverySnapshot from useLangChainSubagents / useLangChainSubgraphs as the target. useLangChainStream returns undefined outside the runtime provider.

import {
  useLangChainStream,
  useLangChainSubagents,
} from "@assistant-ui/react-langchain";
import { useMessages, useToolCalls } from "@langchain/react";

function SubagentCard({ stream, subagent }) {
  const messages = useMessages(stream, subagent);
  const toolCalls = useToolCalls(stream, subagent);
  return (
    <div>
      <strong>{subagent.namespace.join("/")}</strong>
      <pre>{messages.map((m) => m.content).join("\n")}</pre>
      <ul>
        {toolCalls.map((tc) => (
          <li key={tc.id}>{tc.name}</li>
        ))}
      </ul>
    </div>
  );
}

function Subagents() {
  const stream = useLangChainStream();
  const subagents = useLangChainSubagents();
  if (!stream) return null;
  return [...subagents.values()].map((s) => (
    <SubagentCard key={s.namespace.join("/")} stream={stream} subagent={s} />
  ));
}

Per-message metadata

useMessageMetadata (from @langchain/react) reads per-message metadata (e.g. parentCheckpointId) for a given message id. Pair it with useLangChainStream, which returns undefined outside the runtime provider; guard the stream before calling the selector, since useMessageMetadata throws on an undefined stream.

import { useLangChainStream } from "@assistant-ui/react-langchain";
import { useMessageMetadata } from "@langchain/react";

function MessageInfoView({
  stream,
  messageId,
}: {
  stream: NonNullable<ReturnType<typeof useLangChainStream>>;
  messageId: string;
}) {
  const { parentCheckpointId } = useMessageMetadata(stream, messageId) ?? {};
  // …render per-message metadata…
}

function MessageInfo({ messageId }: { messageId: string }) {
  const stream = useLangChainStream();
  if (!stream) return null;
  return <MessageInfoView stream={stream} messageId={messageId} />;
}

Media

The v1 media hooks surface multimodal media streamed by the agent. Feed the useLangChainStream handle to them, as in the views and metadata sections above.

import { useLangChainStream } from "@assistant-ui/react-langchain";
import { useImages, useAudio } from "@langchain/react";

function MediaView({
  stream,
}: {
  stream: NonNullable<ReturnType<typeof useLangChainStream>>;
}) {
  const images = useImages(stream);
  // useAudio / useVideo / useFiles likewise
  return <>{/* …render media… */}</>;
}

function Media() {
  const stream = useLangChainStream();
  if (!stream) return null;
  return <MediaView stream={stream} />;
}

Generative UI

Graphs can accumulate UI components in their state and attach each one to the assistant message that produced it. The runtime reads these from stream.values[uiStateKey] (default "ui") and, for every UIMessage whose parent id matches an assistant message, emits a data part on that message. The parent id is read from metadata.message_id (Python SDK) or metadata.id (JS SDK). Pass uiStateKey if your graph stores them elsewhere.

Register the components with makeAssistantDataUI, keyed by the UIMessage's name, and mount the returned element once inside the runtime provider:

import {
  AssistantRuntimeProvider,
  makeAssistantDataUI,
} from "@assistant-ui/react";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { useStreamRuntime } from "@assistant-ui/react-langchain";

const ChartUI = makeAssistantDataUI({
  name: "chart",
  render: ({ data }) => <Chart points={data.points} />,
});

function App() {
  const runtime = useStreamRuntime({
    assistantId: "agent",
    apiUrl: "http://localhost:2024",
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <ChartUI />
      <Thread />
    </AssistantRuntimeProvider>
  );
}

This covers two paths. The state-snapshot path renders whatever UI the graph has committed to its state. The live path renders UI streamed over the custom channel by push_ui_message / delete_ui_message before (or instead of) it lands in state, so components appear as the graph emits them. The runtime accumulates the live events and merges them with the state snapshot, with the snapshot authoritative once a UI lands in state. Both feed the same makeAssistantDataUI renderers.

The live path requires the graph's streamMode to include "custom" so the UI events reach the client.

Errors

useLangChainError exposes the last run or hydration error from upstream useStream().error. It is untyped (unknown), so narrow it at the use site before rendering.

import { useLangChainError } from "@assistant-ui/react-langchain";

function ErrorBanner() {
  const error = useLangChainError();
  if (!error) return null;
  return <div role="alert">{error instanceof Error ? error.message : String(error)}</div>;
}

Message conversion

convertLangChainBaseMessage transforms a LangChain BaseMessage into an assistant-ui message. Use it when building a custom ExternalStoreAdapter that consumes LangChain messages outside useStreamRuntime.

import { convertLangChainBaseMessage } from "@assistant-ui/react-langchain";

Cloud persistence

Pass an AssistantCloud instance to persist the thread list across sessions. useStream({ threadId: externalId }) loads state from the LangGraph server, not from Assistant Cloud. The runtime copies settled messages to Assistant Cloud for the dashboard; set telemetry: { messages: false } on the cloud client to disable that copy.

// see "AssistantCloud" in /docs/runtimes/concepts/threads for cloud setup
const runtime = useStreamRuntime({
  cloud,
  assistantId: "agent",
  apiUrl: "http://localhost:2024",
});

Custom messagesKey

If your graph stores messages under a non-default key, pass messagesKey so the runtime submits tool results and human turns to the correct state slot:

const runtime = useStreamRuntime({
  assistantId: "agent",
  apiUrl: "http://localhost:2024",
  messagesKey: "chat_messages",
});

Comparison with react-langgraph

Both packages connect assistant-ui to LangGraph backends. They are independent adapters for different upstream libraries; one is not a successor to the other. The official create-assistant-ui template (-t langchain) ships react-langchain. react-langgraph remains the choice for a custom backend stream or an existing react-langgraph app.

Aspectreact-langgraphreact-langchain
Wraps@langchain/langgraph-sdk (raw SDK)@langchain/react (useStream hook)
AgeSept 2024 onwardApril 2026 onward
Version0.14.x0.0.x
Built onuseExternalStoreRuntimeuseExternalStoreRuntime
create-assistant-ui templateNo template-t langchain

Feature coverage

Featurereact-langgraphreact-langchain
Stream messagesYes (useLangGraphRuntime)Yes (useStreamRuntime)
Interrupt stateYesYes
Thread history loading state (thread.isLoading)YesYes
Send raw state update / resume commandYesYes (useLangChainSubmit; useLangChainRespond for payload resume)
Forward per-run runConfigYes (whole object → config)Yes (custom → config.configurable)
Read arbitrary custom state keyYes (useLangGraphState returns the whole state)Yes (useLangChainState<T>(key))
Root tool-calls projectionVia message partsYes (useLangChainToolCalls)
File content parts in messagesYesYes
Regenerate (checkpoint fork)Yes (getCheckpointId)Yes (auto-resolved)
Edit message (checkpoint fork)YesYes (auto-resolved)
Per-message metadata (messages-tuple)YesYes (useMessageMetadata via useLangChainStream)
Multimodal media (audio/image/video/file)Via partsYes (v1 media hooks via useLangChainStream)
Generative UI (state snapshot)YesYes (uiStateKey)
Generative UI (live push_ui_message)YesYes (streamMode: ["custom"])
Subgraph / namespaced stream eventsYes (via eventHandlers)Replaced by useLangChainSubagents / useLangChainSubgraphs + views
Subagent discoveryVia eventHandlersYes (useLangChainSubagents)
Subgraph discoveryVia eventHandlersYes (useLangChainSubgraphs)
Subagent/subgraph message viewsVia eventHandlersYes (via useLangChainStream + v1 selector hooks)
End-to-end cancellation primitiveYes (unstable_createLangGraphStream)n/a; cancellation via useStream().stop() (Cancel button on by default)
Message accumulator utilityYes (LangGraphMessageAccumulator)n/a; useStream owns accumulation
Message queue and steeringYes (unstable_enableMessageQueue)No
Streaming timing (per-message)YesYes
Cloud thread persistenceYesYes

Per-message streaming timing is attached automatically to message.metadata.timing (no setup), matching react-langgraph. It captures time-to-first-token, total stream time, chunk count, and tokens/sec, computed from message growth while the run is in flight.

react-langchain is the newer, thinner wrapper. It delegates to upstream useStream rather than re-implementing the stream plumbing.

Regenerate works without configuration. Clicking regenerate resolves the server checkpoint to fork from (the checkpoint each message records as it streams in, falling back to a stream.client.threads.getHistory(threadId) id match for older turns), then re-runs with submit(null, { forkFrom }). It works on checkpoint-enabled backends (for example LangGraph Cloud); when no checkpoint resolves, regenerate is a no-op. react-langgraph instead requires a user-supplied getCheckpointId callback. Editing a message forks from the same checkpoint and submits the edited human content instead of re-running the prior turn; editing the first message forks from the thread's initial checkpoint to restart it.

Hook name mapping

react-langgraphreact-langchainNotes
useLangGraphRuntimeuseStreamRuntimeOptions extend upstream UseStreamOptions; no stream / create / load to write.
useLangGraphInterruptStateuseLangChainInterruptStateSame return shape.
useLangGraphSendCommanduseLangChainSendCommandSubmits a v1 command ({ resume?, goto?, update? }) via useLangChainSubmit(null, { command }). To carry a response payload, prefer useLangChainRespond.
useLangGraphSenduseLangChainSendSubmits messages via useStream().submit({ [messagesKey]: messages }, options).
useLangGraphMessageMetadata(use useMessageMetadata(useLangChainStream(), id))Per-message metadata via the upstream selector hook.
useLangGraphUIMessages(UI renders as data parts; register with makeAssistantDataUI)See Generative UI.
useLangGraphStateuseLangChainState<T>(key)Reads one custom state key reactively; useLangGraphState returns the whole state object.
(none)useLangChainToolCallsRoot tool calls assembled by useStream.
(none)useLangChainError()Reads the last run/hydration error reactively.
(none)useLangChainRespondAllResume multiple interrupts at one checkpoint (stream.respondAll).
(none)useLangChainInterruptsEvery interrupt pending at the checkpoint, each with id and value.
(via eventHandlers)useLangChainSubagentsSubagents discovered on the current run; replaces subgraph event handlers.
(via eventHandlers)useLangChainSubgraphsSubgraphs discovered on the current run; replaces subgraph event handlers.
(none)useLangChainStreamThe raw useStream handle for v1's scoped useMessages / useToolCalls selector hooks.