Join & rejoin streams
Join and rejoin lets you disconnect from a running agent stream without stopping the agent, then reconnect to it later. The agent continues executing server-side while the client is away, and you pick up the stream exactly where you left off.
import { PatternEmbed } from "/snippets/pattern-embed.jsx"
import RequiresLanggraphServer from '/snippets/oss/requires-langgraph-server.mdx'; import UseStreamTypeInference from '/snippets/oss/use-stream-type-inference.mdx';
Why join & rejoin?
Traditional streaming APIs tightly couple the client and server: if the client disconnects, the stream is lost. Join and rejoin breaks this coupling, enabling several important patterns:
- Network interruptions: mobile users moving between cell towers or Wi-Fi networks can seamlessly resume
- Page navigation: users navigating away from a chat page and returning later without losing progress
- Mobile backgrounding: apps suspended by the OS can rejoin the stream when foregrounded
- Long-running tasks: agents performing multi-minute operations (research, code generation, data analysis) where users don't need to keep the page open
- Multi-device handoff: start a conversation on your phone, rejoin on your desktop
Core concepts
The join/rejoin pattern involves three key mechanisms:
| Method / Option | Purpose |
|---|---|
threadId | Bind the stream to the LangGraph thread you want to observe |
onThreadId | Persist newly-created thread IDs so a remount can reconnect |
stream.disconnect() | Leave the stream client-side while the agent keeps running server-side |
Remount with the same threadId | Reattach to in-flight work for that thread |
Join/rejoin uses
stream.disconnect(), notstream.stop(). By default,stream.stop()cancels the active run: it disconnects the client and cancels the run on the server. For join/rejoin, callstream.disconnect()(alias forstop({ cancel: false })) so the agent continues processing while you are away.To cancel execution explicitly from app code, use
stream.stop()or @[client.runs.cancel].
Setting up useStream
The key setup step is persisting threadId. When the component remounts with
the same thread ID, the stream attaches to the thread's current state and any
in-flight run.
import { useStream } from "@langchain/react";
import { useCallback, useState } from "react";
function Chat() {
const [connected, setConnected] = useState(true);
const [mountKey, setMountKey] = useState(0);
const [threadId, setThreadId] = useState<string | null>(
() => sessionStorage.getItem("activeThreadId"),
);
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId,
onThreadId(id) {
setThreadId(id);
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
const disconnect = useCallback(() => {
void stream.disconnect();
setConnected(false);
}, [stream]);
const rejoin = useCallback(() => {
setMountKey((key) => key + 1);
setConnected(true);
}, []);
return (
<div key={mountKey}>
<ChatControls
stream={stream}
threadId={threadId}
connected={connected}
onDisconnect={disconnect}
onRejoin={rejoin}
/>
</div>
);
}<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { ref } from "vue";
const connected = ref(true);
const mountKey = ref(0);
const threadId = ref<string | null>(sessionStorage.getItem("activeThreadId"));
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId,
onThreadId(id) {
threadId.value = id;
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
function disconnect() {
void stream.disconnect();
connected.value = false;
}
function rejoin() {
mountKey.value += 1;
connected.value = true;
}
</script>
<template>
<div :key="mountKey">
<ChatControls
:stream="stream"
:threadId="threadId"
:connected="connected"
@disconnect="disconnect"
@rejoin="rejoin"
/>
</div>
</template><script lang="ts">
import { useStream } from "@langchain/svelte";
let connected = $state(true);
let mountKey = $state(0);
let threadId = $state<string | null>(sessionStorage.getItem("activeThreadId"));
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "join_rejoin",
threadId: () => threadId,
onThreadId(id) {
threadId = id;
if (id) sessionStorage.setItem("activeThreadId", id);
},
});
function disconnect() {
void stream.disconnect();
connected = false;
}
function rejoin() {
mountKey += 1;
connected = true;
}
</script>
<div key={mountKey}>
<ChatControls
{threadId}
{connected}
onDisconnect={disconnect}
onRejoin={rejoin}
/>
</div>Submitting messages
Submit messages normally. The thread ID binding is what allows a later remount to reconnect to the same conversation:
stream.submit({ messages: [{ type: "human", content: text }] });Disconnecting from a stream
Call stream.disconnect() to leave the stream without cancelling the run. The agent continues processing server-side.
await stream.disconnect();
// equivalent to: await stream.stop({ cancel: false })Do not use stream.stop() here — by default it cancels the run on the server.
After calling disconnect():
stream.isLoadingbecomesfalse- Your own
connectedflag should also becomefalse - The message list retains all messages received up to the disconnect point
- The agent continues running on the server
- No new messages are received until you rejoin
Rejoining a stream
Remount the stream consumer with the saved thread ID to reconnect. In React, the
demo bumps a mountKey; in other frameworks, use the equivalent remount or
conditional-render pattern:
setMountKey((key) => key + 1);
setConnected(true);After rejoining:
connectedbecomestrue- Any messages generated while disconnected are delivered
- New streaming messages resume in real-time
- If the agent is still running,
stream.isLoadingbecomestrue; if it has already finished, you receive the final state immediately
Best practices
- Use
disconnect()for join/rejoin,stop()to cancel: navigating away or backgrounding the app should callstream.disconnect(). A user-facing "Stop" or "Cancel" button should callstream.stop()(or @[client.runs.cancel]). - Always save the thread ID: without it, rejoining is impossible. Use both component state and persistent storage for resilience.
- Show clear connection state: users should always know whether they are receiving live updates or viewing a snapshot.
- Auto-rejoin on visibility change: use the Page Visibility API to automatically rejoin when the user returns to the tab.
- Set reasonable timeouts: if a rejoin attempt takes too long, fall back to fetching the thread history instead.
- Clean up stale threads: remove persisted thread IDs when the user starts over or the backend reports that the thread is unavailable.