This guide walks application authors through the jump from the pre-v1 useStream hook (previously shipped as @langchain/langgraph-sdk/react and later re-exported as @langchain/react) to the useStream hook built for new event-based streaming, which ships with @langchain/react v1.
Short version: the useStream import name does not change, but the return shape, option bag, and protocol semantics do. Most chat apps migrate in well under an hour by following the checklists below. Apps that lean heavily on history / branch / fetchStateHistory or on a custom UseStreamTransport have more work to do.
The legacy useStream was built against the legacy streaming protocol and accreted a large surface of opt-in callbacks (onUpdateEvent, onCustomEvent, onMetadataEvent, …) plus derived state (history, branch, getMessagesMetadata, joinStream) that had to be recomputed on every render.
The v1 package hook uses new event-based streaming. In practice that means:
values / messages / toolCalls / interrupts are always available at the root with zero extra wire cost.typeof agent flows through to values, toolCalls[].args, and subagent-state maps.@langchain/react to ^1.0.0 and @langchain/langgraph-sdk to the matching new event-based streaming runtime.import { useStream } from "@langchain/react" now resolves to the hook built for new event-based streaming. useStreamExperimental is not exported from this package.onError, onFinish, onUpdateEvent, onCustomEvent, onMetadataEvent, onLangChainEvent, onDebugEvent, onCheckpointEvent, onTaskEvent, onToolEvent, onStop, fetchStateHistory, reconnectOnMount, throttle, thread, filterSubagentMessages, subagentToolNames.transport: new FetchStreamTransport(...) with transport: new HttpAgentServerAdapter(...) (see §9).branch, setBranch, history, experimental_branchTree, getMessagesMetadata, toolProgress, joinStream, switchThread, queue, activeSubagents, getSubagent, getSubagentsByType, getSubagentsByMessage.getMessagesMetadata(msg)?.firstSeenState?.parent_checkpoint with useMessageMetadata(stream, msg.id)?.parentCheckpointId (§6).stream.queue with useSubmissionQueue(stream) (§6).stream.switchThread(id) with passing a new threadId prop and letting the hook reload on change (§4).useMessages(stream, subagent) etc.) (§7).submit(..., { onDisconnect, streamResumable }) with stream.stop() (cancel) or stream.disconnect() (join/rejoin) (§5.3).tsc. The option bag and return type are now discriminated and strongly typed.assistantId, client, apiUrl, apiKey, callerOptions, defaultHeaders, threadId, onThreadId, initialValues, messagesKey, onCreated, tools, onTool all keep working.
| Option | Notes |
|---|---|
transport |
"sse" / "websocket" selects the built-in wire transport; an AgentServerAdapter instance flips the hook into the custom branch. |
fetch |
Agent Server branch only. Forwarded to the built-in SSE transport. |
webSocketFactory |
Agent Server branch only. Forwarded to the built-in WebSocket transport. |
onCompleted |
Fires with { runId?, reason } when active streaming ends; runId may be absent for re-attached in-flight runs. |
| Legacy option | v1 replacement |
|---|---|
onError (hook-level) |
Read stream.error directly, or pass a per-submit onError via submit(input, { onError }). |
onFinish |
Use onCompleted, or derive render state from isLoading / useValues(stream). |
onUpdateEvent, onCustomEvent, onMetadataEvent, onLangChainEvent, onDebugEvent, onCheckpointEvent, onTaskEvent, onToolEvent |
Drop. Read raw events via selector hooks (useChannel, useExtension) when you genuinely need them. |
onStop |
Drop. Use stream.stop() to cancel or stream.disconnect() to leave the agent running server-side (§5.3). |
fetchStateHistory |
Drop. Fork/edit flows use useMessageMetadata + submit({}, { forkFrom }) (§5). |
reconnectOnMount |
Drop. Re-attach is automatic. |
throttle |
Drop. The hook batches state updates natively. |
thread |
Drop. External thread managers should drive threadId / initialValues. |
filterSubagentMessages |
Drop. Subagent messages live on per-subagent selector hooks (§7). |
subagentToolNames |
Drop. Subagent classification is driven by new event-based streaming lifecycle events. |
// Before
useStream({ onFinish: (state) => analytics.track("turn_finished", state) });
// After
useStream({
assistantId,
onCompleted: ({ runId, reason }) => analytics.track("turn_finished", { runId, reason }),
});
values, messages, toolCalls, interrupts / interrupt, isLoading, error, threadId, client, assistantId, submit, stop, respond, disconnect. Note submit argument types are wider (§5); stop(options?) cancels server-side by default, disconnect() is join/rejoin client-only (§5.3).
| Field | What changed |
|---|---|
subagents |
Now a ReadonlyMap<string, SubagentDiscoverySnapshot>. Snapshot carries id / name / namespace / status only — read content via selector hooks (§7). |
isThreadLoading |
Reflects the initial thread-load lifecycle rather than fetchStateHistory. |
| Legacy field | v1 replacement |
|---|---|
branch, setBranch, experimental_branchTree |
useMessageMetadata(stream, msg.id) + submit(input, { forkFrom }). |
history, fetchStateHistory |
Fetch explicitly with client.threads.getHistory(threadId) if you need it; most apps do not. |
getMessagesMetadata(msg, i) |
useMessageMetadata(stream, msg.id) returns { parentCheckpointId } (§6). |
toolProgress |
Each AssembledToolCall carries its own status — read via useToolCalls(stream). |
joinStream(runId, ...) |
Remounting the hook with the right threadId rejoins automatically. |
switchThread(newThreadId) |
Drive threadId as a prop. The hook reloads on change. |
queue |
useSubmissionQueue(stream) companion hook (§6). |
activeSubagents, getSubagent, getSubagentsByType, getSubagentsByMessage |
Iterate stream.subagents (a Map) and filter inline. |
subgraphs (ReadonlyMap<string, SubgraphDiscoverySnapshot>) and subgraphsByNode (ReadonlyMap<string, SubgraphDiscoverySnapshot[]>).
// Before
const { messages, isLoading, error, submit, branch, setBranch, getMessagesMetadata } = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
onError: (err) => console.error(err),
fetchStateHistory: true,
});
// After
const stream = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
});
const { messages, isLoading, error, submit } = stream;
useEffect(() => {
if (error) console.error(error);
}, [error]);
const { parentCheckpointId } = useMessageMetadata(stream, messages.at(-1)?.id) ?? {};
submit() signature changessubmit() now accepts either a wire-format message payload or an array of BaseMessage class instances:
await submit({ messages: [{ role: "user", content: "hi" }] });
await submit({ messages: [new HumanMessage("hi")] });
await submit({ messages: new HumanMessage("hi") });
This is driven by the WidenUpdateMessages<T> helper.
Legacy SubmitOptions field |
v1 StreamSubmitOptions equivalent |
|---|---|
context |
Fold into config.configurable. |
checkpoint: { checkpoint_id } |
forkFrom: "cp_123" (direct checkpoint id string). |
command: { resume } |
Use stream.respond() instead. |
interruptBefore, interruptAfter |
Drop — not supported with new event-based streaming. |
multitaskStrategy |
Unchanged. "rollback" (default), "reject", "enqueue" honoured client-side; "interrupt" falls back to "rollback". |
onCompletion |
Use the hook-level onCompleted option. |
onDisconnect, feedbackKeys, streamMode, runId, optimisticValues, streamSubgraphs, streamResumable, checkpointDuring |
Drop from submit. Disconnect/cancel policy now lives on stop() / disconnect() (§5.3). |
(new submit option) onError |
Per-submit fire-and-forget error callback. |
(new) threadId |
Per-submit thread override. |
// Before
await submit(
{ messages: [new HumanMessage("retry")] },
{ checkpoint: { checkpoint_id: "cp_123" }, multitaskStrategy: "rollback" },
);
// After
await submit(
{ messages: [new HumanMessage("retry")] },
{ forkFrom: "cp_123", multitaskStrategy: "rollback" },
);
Legacy stop() only aborted the client transport, and per-submit onDisconnect decided whether the agent kept running. v1 makes the split explicit on the stream handle:
| Legacy pattern | v1 replacement |
|---|---|
| Stop button in a normal chat (cancel the agent) | await stream.stop() — default { cancel: true } calls client.runs.cancel, then disconnects. |
| Join/rejoin — leave the agent running | await stream.disconnect() or await stream.stop({ cancel: false }) |
submit(..., { onDisconnect: "cancel" }) |
Call stream.stop() when the user cancels. |
submit(..., { onDisconnect: "continue", streamResumable: true }) |
Call stream.disconnect() when navigating away; reattach by remounting with the same threadId. |
Legacy useStream returned everything in one object. v1 keeps the always-on data on the root return and pushes the rest into companion selector hooks that ref-count their server subscriptions.
| Hook | Replaces |
|---|---|
useValues(stream) |
stream.values |
useMessages(stream) |
stream.messages |
useToolCalls(stream) |
stream.toolCalls |
useMessageMetadata(stream, msgId) |
stream.getMessagesMetadata(msg, i) |
useSubmissionQueue(stream) |
stream.queue |
useExtension(stream, name) |
Per-event callbacks |
useChannel(stream, channels) |
Raw event callbacks |
useAudio / useImages / useVideo / useFiles |
— |
// Before: everything on the root
const { messages, toolCalls, getMessagesMetadata, queue } = useStream({ assistantId });
// After: always-on stays on the root; rest moves to selectors
const stream = useStream({ assistantId });
const messages = useMessages(stream); // or just stream.messages
const metadata = useMessageMetadata(stream, messages.at(-1)?.id);
const { entries, size, cancel, clear } = useSubmissionQueue(stream);
Subagents and subgraphs are now discovered eagerly but streamed lazily. The discovery maps (stream.subagents, stream.subgraphs, stream.subgraphsByNode) carry identity fields only — no messages / toolCalls / values. Read those via selector hooks, passing the discovery snapshot as target:
// Before
{
[...stream.subagents.values()].map((s) => (
<SubagentCard key={s.id} messages={s.messages} toolCalls={s.toolCalls} />
));
}
// After
{
[...stream.subagents.values()].map((s) => (
<SubagentCard key={s.id} stream={stream} subagent={s} />
));
}
function SubagentCard({ stream, subagent }) {
const messages = useMessages(stream, subagent);
const toolCalls = useToolCalls(stream, subagent);
const values = useValues<ResearcherState>(stream, subagent);
}
tools + onTool)The legacy tools / onTool options are preserved one-for-one. No migration is needed if you were already using this API. The helper exports (flushPendingHeadlessToolInterrupts, findHeadlessTool, handleHeadlessToolInterrupt, …) are still available from @langchain/react.
UseStreamTransport → AgentServerAdapterThe legacy UseStreamTransport interface and FetchStreamTransport class are replaced by AgentServerAdapter and the convenience HttpAgentServerAdapter (SSE + WS with injectable fetch / webSocketFactory / defaultHeaders).
// Before
import { FetchStreamTransport, useStream } from "@langchain/react";
const transport = new FetchStreamTransport({ apiUrl: "/api/chat" });
const stream = useStream({ transport });
// After
import { HttpAgentServerAdapter, useStream } from "@langchain/react";
const transport = new HttpAgentServerAdapter({
apiUrl: "/api/chat",
threadId: "thread-123", // required: the adapter is bound to a thread
defaultHeaders: { Authorization: `Bearer ${token}` },
});
const stream = useStream({ transport });
Passing both assistantId + apiUrl and a transport: AgentServerAdapter is now a compile-time error. See Transports for the full interface.
StreamProvider / useStreamContextThe provider and consumer are unchanged at the call site. Because the underlying useStream changed, the context value changes accordingly — update any destructuring per §4. StreamProviderProps<T> (Agent Server branch) and StreamProviderCustomProps<T> (custom-adapter branch) mirror the two arms of the options union.
useSuspenseStreamuseSuspenseStream is now a slim port aligned with the v1 package API, built on top of useStream and the controller's hydrationPromise.
| Legacy surface | v1 replacement |
|---|---|
SuspenseCache, createSuspenseCache, invalidateSuspenseCache |
Gone. The hook uses a module-level cache keyed on (apiUrl, assistantId, threadId). |
suspenseCache option |
Gone. No caller-side setup required. |
fetchStateHistory: { limit } prefetch |
Gone. The hook hydrates via threads.getState() and suspends until that settles. |
branch / setBranch / history / getMessagesMetadata on the return |
Gone, same as plain useStream. Use the companion hooks (§6). |
UseSuspenseStreamReturn<T> is UseStreamReturn<T> minus isLoading / isThreadLoading / hydrationPromise, plus isStreaming: boolean. Non-streaming errors are thrown to the nearest Error Boundary.
| Helper | Use |
|---|---|
UseStreamReturn<T> (alias: UseStreamResult<T>) |
The fully-resolved return type of useStream<T>. Prop-drill as { stream: UseStreamReturn<typeof agent> }. |
AnyStream |
Type-erased handle (UseStreamReturn<any, any, any>). |
InferStateType<T> |
Unwraps a compiled graph / agent brand / agent tool array into its state shape. |
InferToolCalls<T> |
Derives a discriminated union of tool-call shapes. |
InferSubagentStates<T> |
{ name: State, … } map derived from a DeepAgent brand. |
WidenUpdateMessages<T> |
Widens messages in a partial state update. |
StreamSubmitOptions<State, Configurable> |
Options shape accepted by submit(). |
AgentServerAdapter, HttpAgentServerAdapter, HttpAgentServerAdapterOptions |
Custom-transport interface + convenience class. |
UseStreamOptions, AgentServerOptions, CustomAdapterOptions |
Discriminated options union; rarely needed at call sites. |
Breaking change: the legacy type aliases re-exported from @langchain/react are no longer available from this package: UseStream, UseSuspenseStream, UseStreamCustom, UseStreamCustomOptions, UseStreamTransport, UseStreamThread, GetToolCallsType, QueueEntry, QueueInterface, SubagentStream, FetchStreamTransport, and others.
| Legacy name | v1 replacement |
|---|---|
UseStream<State, Bag> |
UseStreamReturn<State> (or UseStreamReturn<typeof agent>). |
UseStreamOptions<State, Bag> |
UseStreamOptions<State> (or just let the hook infer). |
UseStreamTransport |
AgentServerAdapter (§9). |
FetchStreamTransport |
HttpAgentServerAdapter (§9). |
GetToolCallsType<State> |
InferToolCalls<typeof agent>. |
UseSuspenseStream<…> |
UseSuspenseStreamReturn<T> (§11). |
QueueEntry, QueueInterface |
SubmissionQueueEntry, UseSubmissionQueueReturn (§6). |
SubagentStream, SubagentStreamInterface |
SubagentDiscoverySnapshot + useMessages(stream, subagent) (§7). |
Apps that cannot migrate all call sites in one pass can keep using the legacy types by importing them directly from @langchain/langgraph-sdk/ui during the transition.
MessageMetadata collisionBreaking change: v1 exports a new MessageMetadata from @langchain/langgraph-sdk/stream with shape { parentCheckpointId }, different from the legacy one ({ messageId, firstSeenState, branch, branchOptions }). Legacy call sites must import the legacy type from @langchain/langgraph-sdk/ui.
Some v1 features are accepted but not yet executed end-to-end on all servers:
| Feature | Status today |
|---|---|
submit(input, { forkFrom }) |
Type-accepted; forwarded on run.start. |
multitaskStrategy: "enqueue" |
Fully honoured client-side; drains queued entries sequentially. |
multitaskStrategy: "reject" |
Fully honoured client-side — submit() throws when a run is already in flight. |
multitaskStrategy: "rollback" (default) |
Fully honoured client-side — in-flight run aborted, new submission dispatched. |
multitaskStrategy: "interrupt" |
Type-accepted. Falls back to "rollback" until server-side semantics land. |
useMessageMetadata().parentCheckpointId |
Populated from the parent_checkpoint field on values events. |
We still need a raw event stream for analytics. What replaces onLangChainEvent / onDebugEvent / onCustomEvent?
Use useChannel(stream, channels) for a bounded buffer of raw events scoped to a namespace, or useExtension(stream, name) for a specific extension. For app-wide telemetry, tee events through a custom AgentServerAdapter (§9).
My backend only emits values events (no messages channel). Will streaming still work?
Yes — stream.messages merges messages-channel deltas and values.messages snapshots. Backends that only emit values render full turns at once instead of token-by-token.
We pinned @langchain/langgraph-sdk in app code. Do we need to bump it?
Yes. @langchain/react v1 depends on the new event-based streaming runtime in @langchain/langgraph-sdk.
How do I migrate a useStream call that was deeply generic (useStream<State, Bag>)?
v1 takes three generics: useStream<T, InterruptType, ConfigurableType> where T is either a plain state shape or an agent brand. The legacy Bag options are gone; if you passed InterruptType via Bag, lift it to the second generic slot.
// Before
useStream<MyState, { InterruptType: MyInterrupt }>({ ... });
// After
useStream<MyState, MyInterrupt>({ ... });