The useStream React hook provides seamless integration with LangGraph streaming capabilities. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great generative UI experiences.
Key features:
Messages streaming — Handle a stream of message chunks to form a complete message
Automatic state management — for messages, interrupts, loading states, and errors
Conversation branching — Create alternate conversation paths from any point in the chat history
UI-agnostic design — Bring your own components and styling
Installation
Install the LangGraph SDK to use the useStream hook in your React application:
npm install @langchain/langgraph-sdk
Basic usage
The useStream hook connects to any LangGraph graph, whether that’s running on from your own endpoint, or deployed using LangSmith deployments .
import { useStream } from "@langchain/langgraph-sdk/react" ;
function Chat () {
const stream = useStream ({
assistantId: "agent" ,
// Local development
apiUrl: "http://localhost:2024" ,
// Production deployment (LangSmith hosted)
// apiUrl: "https://your-deployment.us.langgraph.app"
});
const handleSubmit = ( message : string ) => {
stream . submit ({
messages: [
{ content: message , type: "human" }
],
});
};
return (
< div >
{ stream . messages . map (( message , idx ) => (
< div key = { message . id ?? idx } >
{ message . type } : { message . content }
</ div >
)) }
{ stream . isLoading && < div > Loading... </ div > }
{ stream . error && < div > Error: { stream . error . message } </ div > }
</ div >
);
}
The ID of the agent to connect to. When using LangSmith deployments, this must match the agent ID shown in your deployment dashboard. For custom API deployments or local development, this can be any string that your server uses to identify the agent.
The URL of the LangGraph server. Defaults to http://localhost:2024 for local development.
API key for authentication. Required when connecting to deployed agents on LangSmith.
Connect to an existing thread instead of creating a new one. Useful for resuming conversations.
Callback invoked when a new thread is created. Use this to persist the thread ID for later use.
reconnectOnMount
boolean | (() => Storage)
Automatically resume an ongoing run when the component mounts. Set to true to use session storage, or provide a custom storage function.
Callback invoked when a new run is created. Useful for persisting run metadata for resumption.
Callback invoked when an error occurs during streaming.
onFinish
(state: StateType, run?: Run) => void
Callback invoked when the stream completes successfully with the final state.
onCustomEvent
(data: unknown, context: { mutate }) => void
onUpdateEvent
(data: unknown, context: { mutate }) => void
Handle state update events after each graph step.
onMetadataEvent
(metadata: { run_id, thread_id }) => void
Handle metadata events with run and thread information.
The key in the graph state that contains the messages array.
Batch state updates for better rendering performance. Disable for immediate updates.
Initial state values to display while the first stream is loading. Useful for showing cached thread data immediately.
`useStream` return values
All messages in the current thread, including both human and AI messages.
The current graph state values. Type is inferred from the agent or graph type parameter.
Whether a stream is currently in progress. Use this to show loading indicators.
Any error that occurred during streaming. null when no error.
Current interrupt requiring user input, such as human-in-the-loop approval requests.
All tool calls across all messages, with their results and state (pending, completed, or error).
submit
(input, options?) => Promise<void>
Submit new input to the agent. Pass null as input when resuming from an interrupt with a command. Options include checkpoint for branching, optimisticValues for optimistic updates, and threadId for optimistic thread creation.
Stop the current stream immediately.
Resume an existing stream by run ID. Use with onCreated for manual stream resumption.
Switch to a different branch in the conversation history.
Get all tool calls for a specific AI message.
getMessagesMetadata
(message) => MessageMetadata
Get metadata for a message, including streaming info like langgraph_node for identifying the source node, and firstSeenState for branching.
Tree representation of the thread for advanced branching controls in non-message based graphs.
Thread management
Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:
import { useState } from "react" ;
import { useStream } from "@langchain/langgraph-sdk/react" ;
function Chat () {
const [ threadId , setThreadId ] = useState < string | null >( null );
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
threadId: threadId ,
onThreadId: setThreadId ,
});
// threadId is updated when a new thread is created
// Store it in URL params or localStorage for persistence
}
We recommend storing the threadId to let users resume conversations after page refreshes.
Resume after page refresh
The useStream hook can automatically resume an ongoing run upon mounting by setting reconnectOnMount: true. This is useful for continuing a stream after a page refresh, ensuring no messages and events generated during the downtime are lost.
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
reconnectOnMount: true ,
});
By default the ID of the created run is stored in window.sessionStorage, which can be swapped by passing a custom storage function:
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
reconnectOnMount : () => window . localStorage ,
});
For manual control over the resumption process, use the run callbacks to persist metadata and joinStream to resume:
import { useStream } from "@langchain/langgraph-sdk/react" ;
import { useEffect , useRef } from "react" ;
function Chat ({ threadId } : { threadId : string | null }) {
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
threadId ,
onCreated : ( run ) => {
// Persist run ID when stream starts
window . sessionStorage . setItem ( `resume: ${ run . thread_id } ` , run . run_id );
},
onFinish : ( _ , run ) => {
// Clean up when stream completes
window . sessionStorage . removeItem ( `resume: ${ run ?. thread_id } ` );
},
});
// Resume stream on mount if there's a stored run ID
const joinedThreadId = useRef < string | null >( null );
useEffect (() => {
if ( ! threadId ) return ;
const runId = window . sessionStorage . getItem ( `resume: ${ threadId } ` );
if ( runId && joinedThreadId . current !== threadId ) {
stream . joinStream ( runId );
joinedThreadId . current = threadId ;
}
}, [ threadId ]);
const handleSubmit = ( text : string ) => {
// Use streamResumable to ensure events aren't lost
stream . submit (
{ messages: [{ type: "human" , content: text }] },
{ streamResumable: true }
);
};
}
Try the session persistence example See a complete implementation of stream resumption with reconnectOnMount and thread persistence in the session-persistence example.
Optimistic updates
You can optimistically update the client state before performing a network request, providing immediate feedback to the user:
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
});
const handleSubmit = ( text : string ) => {
const newMessage = { type: "human" as const , content: text };
stream . submit (
{ messages: [ newMessage ] },
{
optimisticValues ( prev ) {
const prevMessages = prev . messages ?? [];
return { ... prev , messages: [ ... prevMessages , newMessage ] };
},
}
);
};
Optimistic thread creation
Use the threadId option in submit to enable optimistic UI patterns where you need to know the thread ID before the thread is created:
import { useState } from "react" ;
import { useStream } from "@langchain/langgraph-sdk/react" ;
function Chat () {
const [ threadId , setThreadId ] = useState < string | null >( null );
const [ optimisticThreadId ] = useState (() => crypto . randomUUID ());
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
threadId ,
onThreadId: setThreadId ,
});
const handleSubmit = ( text : string ) => {
// Navigate immediately without waiting for thread creation
window . history . pushState ({}, "" , `/threads/ ${ optimisticThreadId } ` );
// Create thread with the predetermined ID
stream . submit (
{ messages: [{ type: "human" , content: text }] },
{ threadId: optimisticThreadId }
);
};
}
Cached thread display
Use the initialValues option to display cached thread data immediately while the history is being loaded from the server:
function Chat ({ threadId , cachedData }) {
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
threadId ,
initialValues: cachedData ?. values ,
});
// Shows cached messages instantly, then updates when server responds
}
Branching
Create alternate conversation paths by editing previous messages or regenerating AI responses. Use getMessagesMetadata() to access checkpoint information for branching:
Chat.tsx
BranchSwitcher.tsx
import { useStream } from "@langchain/langgraph-sdk/react" ;
import { BranchSwitcher } from "./BranchSwitcher" ;
function Chat () {
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
});
return (
< div >
{ stream . messages . map (( message ) => {
const meta = stream . getMessagesMetadata ( message );
const parentCheckpoint = meta ?. firstSeenState ?. parent_checkpoint ;
return (
< div key = { message . id } >
< div > { message . content as string } </ div >
{ /* Edit human messages */ }
{ message . type === "human" && (
< button
onClick = { () => {
const newContent = prompt ( "Edit message:" , message . content as string );
if ( newContent ) {
stream . submit (
{ messages: [{ type: "human" , content: newContent }] },
{ checkpoint: parentCheckpoint }
);
}
} }
>
Edit
</ button >
) }
{ /* Regenerate AI messages */ }
{ message . type === "ai" && (
< button
onClick = { () => stream . submit ( undefined , { checkpoint: parentCheckpoint }) }
>
Regenerate
</ button >
) }
{ /* Switch between branches */ }
< BranchSwitcher
branch = { meta ?. branch }
branchOptions = { meta ?. branchOptions }
onSelect = { ( branch ) => stream . setBranch ( branch ) }
/>
</ div >
);
}) }
</ div >
);
}
For advanced use cases, use the experimental_branchTree property to get the tree representation of the thread for non-message based graphs.
Try the branching example See a complete implementation of conversation branching with edit, regenerate, and branch switching in the branching-chat example.
Type-safe streaming
The useStream hook supports full type inference when used with agents created via createAgent or graphs created with StateGraph . Pass typeof agent or typeof graph as the type parameter to automatically infer tool call types.
With createAgent
When using createAgent , tool call types are automatically inferred from the tools you register to your agent:
import { createAgent , tool } from "langchain" ;
import { z } from "zod" ;
const getWeather = tool (
async ({ location }) => `Weather in ${ location } : Sunny, 72°F` ,
{
name: "get_weather" ,
description: "Get weather for a location" ,
schema: z . object ({
location: z . string (). describe ( "The city to get weather for" ),
}),
}
);
export const agent = createAgent ({
model: "openai:gpt-4.1-mini" ,
tools: [ getWeather ],
});
With StateGraph
For custom StateGraph applications, the state types are inferred from the graph’s annotation:
import { StateGraph , MessagesAnnotation , START , END } from "@langchain/langgraph" ;
import { ChatOpenAI } from "@langchain/openai" ;
const model = new ChatOpenAI ({ model: "gpt-4.1-mini" });
const workflow = new StateGraph ( MessagesAnnotation )
. addNode ( "agent" , async ( state ) => {
const response = await model . invoke ( state . messages );
return { messages: [ response ] };
})
. addEdge ( START , "agent" )
. addEdge ( "agent" , END );
export const graph = workflow . compile ();
With Annotation types
If you’re using LangGraph.js, you can reuse your graph’s annotation types. Make sure to only import types to avoid importing the entire LangGraph.js runtime:
import {
Annotation ,
MessagesAnnotation ,
type StateType ,
type UpdateType ,
} from "@langchain/langgraph/web" ;
const AgentState = Annotation . Root ({
... MessagesAnnotation . spec ,
context: Annotation < string >(),
});
const stream = useStream <
StateType < typeof AgentState . spec > ,
{ UpdateType : UpdateType < typeof AgentState . spec > }
> ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
});
Advanced type configuration
You can specify additional type parameters for interrupts, custom events, and configurable options:
import type { Message } from "@langchain/langgraph-sdk" ;
type State = { messages : Message []; context ?: string };
const stream = useStream <
State ,
{
UpdateType : { messages : Message [] | Message ; context ?: string };
InterruptType : string ;
CustomEventType : { type : "progress" | "debug" ; payload : unknown };
ConfigurableType : { model : string };
}
> ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
});
// stream.interrupt is typed as string | undefined
// onCustomEvent receives typed events
Use getToolCalls to extract and render tool calls from AI messages. Tool calls include the call details, result (if completed), and state.
Chat.tsx
ToolCallCard.tsx
GenericToolCallCard.tsx
WeatherCard.tsx
tools.ts
utils.ts
import { useStream } from "@langchain/langgraph-sdk/react" ;
import type { agent } from "./agent" ;
import { ToolCallCard } from "./ToolCallCard" ;
import { MessageBubble } from "./MessageBubble" ;
function Chat () {
const stream = useStream < typeof agent >({
assistantId: "agent" ,
apiUrl: "http://localhost:2024" ,
});
return (
< div className = "flex flex-col gap-4" >
{ stream . messages . map (( message , idx ) => {
if ( message . type === "ai" ) {
const toolCalls = stream . getToolCalls ( message );
if ( toolCalls . length > 0 ) {
return (
< div key = { message . id ?? idx } className = "flex flex-col gap-2" >
{ toolCalls . map (( toolCall ) => (
< ToolCallCard key = { toolCall . id } toolCall = { toolCall } />
)) }
</ div >
);
}
}
return < MessageBubble key = { message . id ?? idx } message = { message } /> ;
}) }
</ div >
);
}
Try the tool calling example See a complete implementation of tool call rendering with weather, calculator, and note-taking tools in the tool-calling-agent example.
Custom streaming events
Stream custom data from your agent using the writer in your tools or nodes. Handle these events in the UI with the onCustomEvent callback.
import { tool , type ToolRuntime } from "langchain" ;
import { z } from "zod" ;
// Define your custom event types
interface ProgressData {
type : "progress" ;
id : string ;
message : string ;
progress : number ;
}
const analyzeDataTool = tool (
async ({ dataSource }, config : ToolRuntime ) => {
const steps = [ "Connecting..." , "Fetching..." , "Processing..." , "Done!" ];
for ( let i = 0 ; i < steps . length ; i ++ ) {
// Emit progress events during execution
config . writer ?.({
type: "progress" ,
id: `analysis- ${ Date . now () } ` ,
message: steps [ i ],
progress: (( i + 1 ) / steps . length ) * 100 ,
} satisfies ProgressData );
await new Promise (( resolve ) => setTimeout ( resolve , 500 ));
}
return JSON . stringify ({ result: "Analysis complete" });
},
{
name: "analyze_data" ,
description: "Analyze data with progress updates" ,
schema: z . object ({
dataSource: z . string (). describe ( "Data source to analyze" ),
}),
}
);
Try the custom streaming example See a complete implementation of custom events with progress bars, status badges, and file operation cards in the custom-streaming example.
Event handling
The useStream hook provides callback options that give you access to different types of streaming events. You don’t need to explicitly configure stream modes—just pass callbacks for the event types you want to handle:
const stream = useStream ({
apiUrl: "http://localhost:2024" ,
assistantId: "agent" ,
// Handle state updates after each graph step
onUpdateEvent : ( update , options ) => {
console . log ( "Graph update:" , update );
},
// Handle custom events streamed from your graph
onCustomEvent : ( event , options ) => {
console . log ( "Custom event:" , event );
},
// Handle metadata events with run/thread info
onMetadataEvent : ( metadata ) => {
console . log ( "Run ID:" , metadata . run_id );
console . log ( "Thread ID:" , metadata . thread_id );
},
onError : ( error ) => {
console . error ( "Stream error:" , error );
},
onFinish : ( state , options ) => {
console . log ( "Stream finished with final state:" , state );
},
});
Available callbacks
Callback Description Stream mode onUpdateEventCalled when a state update is received after each graph step updatesonCustomEventCalled when a custom event is received from your graph customonMetadataEventCalled with run and thread metadata metadataonErrorCalled when an error occurs - onFinishCalled when the stream completes -
Multi-agent streaming
When working with multi-agent systems or graphs with multiple nodes, use message metadata to identify which node generated each message. This is particularly useful when multiple LLMs run in parallel and you want to display their outputs with distinct visual styling.
import { useStream } from "@langchain/langgraph-sdk/react" ;
import type { agent } from "./agent" ;
import { MessageBubble } from "./MessageBubble" ;
// Node configuration for visual display
const NODE_CONFIG : Record < string , { label : string ; color : string }> = {
researcher_analytical: { label: "Analytical Research" , color: "cyan" },
researcher_creative: { label: "Creative Research" , color: "purple" },
researcher_practical: { label: "Practical Research" , color: "emerald" },
};
function MultiAgentChat () {
const stream = useStream < typeof agent >({
assistantId: "parallel-research" ,
apiUrl: "http://localhost:2024" ,
});
return (
< div className = "flex flex-col gap-4" >
{ stream . messages . map (( message , idx ) => {
if ( message . type !== "ai" ) {
return < MessageBubble key = { message . id ?? idx } message = { message } /> ;
}
// Get streaming metadata to identify the source node
const metadata = stream . getMessagesMetadata ?.( message );
const nodeName =
( metadata ?. streamMetadata ?. langgraph_node as string ) ||
( message as { name ?: string }). name ;
const config = nodeName ? NODE_CONFIG [ nodeName ] : null ;
if ( ! config ) {
return < MessageBubble key = { message . id ?? idx } message = { message } /> ;
}
return (
< div
key = { message . id ?? idx }
className = { `bg- ${ config . color } -950/30 border border- ${ config . color } -500/30 rounded-xl p-4` }
>
< div className = { `text-sm font-semibold text- ${ config . color } -400 mb-2` } >
{ config . label }
</ div >
< div className = "text-neutral-200 whitespace-pre-wrap" >
{typeof message . content === "string" ? message . content : "" }
</ div >
</ div >
);
}) }
</ div >
);
}
Try the parallel research example See a complete implementation of multi-agent streaming with three parallel researchers and distinct visual styling in the parallel-research example.
Human-in-the-loop
Handle interrupts when the agent requires human approval for tool execution. Learn more in the How to handle interrupts guide.
import { useState } from "react" ;
import { useStream } from "@langchain/langgraph-sdk/react" ;
import type { HITLRequest , HITLResponse } from "langchain" ;
import type { agent } from "./agent" ;
import { MessageBubble } from "./MessageBubble" ;
function HumanInTheLoopChat () {
const stream = useStream < typeof agent , { InterruptType : HITLRequest }>({
assistantId: "human-in-the-loop" ,
apiUrl: "http://localhost:2024" ,
});
const [ isProcessing , setIsProcessing ] = useState ( false );
// Type assertion for interrupt value
const hitlRequest = stream . interrupt ?. value as HITLRequest | undefined ;
const handleApprove = async ( index : number ) => {
if ( ! hitlRequest ) return ;
setIsProcessing ( true );
try {
const decisions : HITLResponse [ "decisions" ] =
hitlRequest . actionRequests . map (( _ , i ) =>
i === index ? { type: "approve" } : { type: "approve" }
);
await stream . submit ( null , {
command: {
resume: { decisions } as HITLResponse ,
},
});
} finally {
setIsProcessing ( false );
}
};
const handleReject = async ( index : number , reason : string ) => {
if ( ! hitlRequest ) return ;
setIsProcessing ( true );
try {
const decisions : HITLResponse [ "decisions" ] =
hitlRequest . actionRequests . map (( _ , i ) =>
i === index
? { type: "reject" , message: reason }
: { type: "reject" , message: "Rejected along with other actions" }
);
await stream . submit ( null , {
command: {
resume: { decisions } as HITLResponse ,
},
});
} finally {
setIsProcessing ( false );
}
};
return (
< div >
{ /* Render messages */ }
{ stream . messages . map (( message , idx ) => (
< MessageBubble key = { message . id ?? idx } message = { message } />
)) }
{ /* Render approval UI when interrupted */ }
{ hitlRequest && hitlRequest . actionRequests . length > 0 && (
< div className = "bg-amber-900/20 border border-amber-500/30 rounded-xl p-4 mt-4" >
< h3 className = "text-amber-400 font-semibold mb-4" >
Action requires approval
</ h3 >
{ hitlRequest . actionRequests . map (( action , idx ) => (
< div
key = { idx }
className = "bg-neutral-900 rounded-lg p-4 mb-4 last:mb-0"
>
< div className = "flex items-center gap-2 mb-2" >
< span className = "text-sm font-mono text-white" >
{ action . name }
</ span >
</ div >
< pre className = "text-xs bg-black rounded p-2 mb-3 overflow-x-auto" >
{ JSON . stringify ( action . args , null , 2 ) }
</ pre >
< div className = "flex gap-2" >
< button
onClick = { () => handleApprove ( idx ) }
disabled = { isProcessing }
className = "px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm rounded disabled:opacity-50"
>
Approve
</ button >
< button
onClick = { () => handleReject ( idx , "User rejected" ) }
disabled = { isProcessing }
className = "px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white text-sm rounded disabled:opacity-50"
>
Reject
</ button >
</ div >
</ div >
)) }
</ div >
) }
</ div >
);
}
Try the human-in-the-loop example See a complete implementation of approval workflows with approve, reject, and edit actions in the human-in-the-loop example.
Reasoning models
Extended reasoning/thinking support is currently experimental. The streaming interface for reasoning tokens varies by provider (OpenAI vs. Anthropic) and may change as abstractions are developed.
When using models with extended reasoning capabilities (like OpenAI’s reasoning models or Anthropic’s extended thinking), the thinking process is embedded in the message content. You’ll need to extract and display it separately.
import { useStream } from "@langchain/langgraph-sdk/react" ;
import type { Message } from "@langchain/langgraph-sdk" ;
import type { agent } from "./agent" ;
import { getReasoningFromMessage , getTextContent } from "./utils" ;
function ReasoningChat () {
const stream = useStream < typeof agent >({
assistantId: "reasoning-agent" ,
apiUrl: "http://localhost:2024" ,
});
return (
< div className = "flex flex-col gap-4" >
{ stream . messages . map (( message , idx ) => {
if ( message . type === "ai" ) {
const reasoning = getReasoningFromMessage ( message );
const textContent = getTextContent ( message );
return (
< div key = { message . id ?? idx } >
{ /* Render reasoning bubble if present */ }
{ reasoning && (
< div className = "mb-4" >
< div className = "text-xs font-medium text-amber-400/80 mb-2" >
Reasoning
</ div >
< div className = "bg-amber-950/50 border border-amber-500/20 rounded-2xl px-4 py-3" >
< div className = "text-sm text-amber-100/90 whitespace-pre-wrap" >
{ reasoning }
</ div >
</ div >
</ div >
) }
{ /* Render text content */ }
{ textContent && (
< div className = "text-neutral-100 whitespace-pre-wrap" >
{ textContent }
</ div >
) }
</ div >
);
}
return < MessageBubble key = { message . id ?? idx } message = { message } /> ;
}) }
{ stream . isLoading && (
< div className = "flex items-center gap-2 text-amber-400/70" >
< span className = "text-sm" > Thinking... </ span >
</ div >
) }
</ div >
);
}
Try the reasoning example See a complete implementation of reasoning token display with OpenAI and Anthropic models in the reasoning-agent example.
Custom state types
For custom LangGraph applications, embed your tool call types in your state’s messages property.
import { Message } from "@langchain/langgraph-sdk" ;
import { useStream } from "@langchain/langgraph-sdk/react" ;
// Define your tool call types as a discriminated union
type MyToolCalls =
| { name : "search" ; args : { query : string }; id ?: string }
| { name : "calculate" ; args : { expression : string }; id ?: string };
// Embed tool call types in your state's messages
interface MyGraphState {
messages : Message < MyToolCalls >[];
context ?: string ;
}
function CustomGraphChat () {
const stream = useStream < MyGraphState >({
assistantId: "my-graph" ,
apiUrl: "http://localhost:2024" ,
});
// stream.values is typed as MyGraphState
// stream.toolCalls[0].call.name is typed as "search" | "calculate"
}
You can also specify additional type configuration for interrupts and configurable options:
interface MyGraphState {
messages : Message < MyToolCalls >[];
}
function CustomGraphChat () {
const stream = useStream <
MyGraphState ,
{
InterruptType : { question : string };
ConfigurableType : { userId : string };
}
> ({
assistantId: "my-graph" ,
apiUrl: "http://localhost:2024" ,
});
// stream.interrupt is typed as { question: string } | undefined
}
Custom transport
For custom API endpoints or non-standard deployments, use the transport option with FetchStreamTransport to connect to any streaming API.
import { useMemo } from "react" ;
import { useStream , FetchStreamTransport } from "@langchain/langgraph-sdk/react" ;
function CustomAPIChat ({ apiKey } : { apiKey : string }) {
// Create transport with custom request handling
const transport = useMemo (() => {
return new FetchStreamTransport ({
apiUrl: "/api/my-agent" ,
onRequest : async ( url : string , init : RequestInit ) => {
// Inject API key or other custom data into requests
const customBody = JSON . stringify ({
... ( JSON . parse ( init . body as string ) || {}),
apiKey ,
});
return {
... init ,
body: customBody ,
headers: {
... init . headers ,
"X-Custom-Header" : "value" ,
},
};
},
});
}, [ apiKey ]);
const stream = useStream ({
transport ,
});
// Use stream as normal
return (
< div >
{ stream . messages . map (( message , idx ) => (
< MessageBubble key = { message . id ?? idx } message = { message } />
)) }
</ div >
);
}
Example: Streaming from Next.js endpoints
You can host your agent in Next.js API routes instead of running a separate LangGraph server.The useStream hook communicates via Server-Sent Events (SSE), so any endpoint that returns the correct event format works—including your own Next.js route.
When to use your own API routes vs LangSmith For basic agent interaction (streaming, tool calls), rolling your own Next.js API routes works well. For persisting conversations, loading thread history, and conversation branching, consider LangSmith deployments , which provide these features out of the box.
Server: Stream from an API route
In your Next.js API route, stream the agent’s output by returning a Response with text/event-stream encoding:
// app/api/agent/route.ts
import { NextRequest } from "next/server" ;
import { createAgent , tool } from "langchain" ;
import { ChatAnthropic } from "@langchain/anthropic" ;
import { MemorySaver } from "@langchain/langgraph" ;
import type { BaseMessage } from "@langchain/core/messages" ;
const checkpointer = new MemorySaver ();
export async function POST ( request : NextRequest ) {
const body = await request . json ();
const agent = createAgent ({
model: new ChatAnthropic ({ model: "claude-sonnet-4-5-20250929" }),
tools: [ /* your tools */ ],
checkpointer ,
});
const stream = await agent . stream (
{ messages: body . messages },
{
encoding: "text/event-stream" ,
streamMode: [ "values" , "updates" , "messages" ],
configurable: body . config ?. configurable ,
recursionLimit: 10 ,
}
);
return new Response ( stream , {
headers: { "Content-Type" : "text/event-stream" },
});
}
Client: Connect with FetchStreamTransport
Use FetchStreamTransport to point useStream at your Next.js API route. Pass transport instead of apiUrl and assistantId when using a custom endpoint:
import { useMemo } from "react" ;
import { useStream , FetchStreamTransport } from "@langchain/langgraph-sdk/react" ;
function ChatInterface ({ apiKey } : { apiKey : string }) {
const transport = useMemo (() => {
return new FetchStreamTransport ({
apiUrl: "/api/agent" ,
onRequest : async ( url : string , init : RequestInit ) => {
// Inject API key or other data into the request body
const customBody = JSON . stringify ({
... ( JSON . parse ( init . body as string ) || {}),
apiKey ,
});
return { ... init , body: customBody };
},
});
}, [ apiKey ]);
const stream = useStream ({
transport ,
});
return (
< div >
{ stream . messages . map (( message , idx ) => (
< div key = { message . id ?? idx } > { /* render message */ } </ div >
)) }
{ stream . isLoading && < div > Loading... </ div > }
</ div >
);
}
For thread history, pass threadId to useStream and include it in configurable when streaming from your API route. The agent’s checkpointer will load and persist state per thread.
Full Next.js example See a complete Next.js app with LangChain agents, streaming chat, and tool calling in the langchain-nextjs repository.