Queueing decides what happens when the user sends a message while a reply is still streaming: wait, ignore it, or stop the current run.
useChat has a message queue for this. By default, the new message waits and sends after the current run succeeds.
useChat returns queue apart from messages. cancelQueued(id) removes an item before it sends:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, queue, sendMessage, cancelQueued } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
queue: { whenBusy: "queue", maxSize: 5 },
});
return (
<>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, index) =>
part.type === "text" ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
{queue.map((item) => (
<div key={item.id} className="pending">
{typeof item.content === "string" ? item.content : "[attachment]"}
<button type="button" onClick={() => cancelQueued(item.id)}>
Cancel
</button>
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault();
if (input.trim() === "") {
return;
}
sendMessage(input);
setInput("");
}}
>
<input
value={input}
onChange={(event) => setInput(event.target.value)}
/>
<button type="submit">Send</button>
</form>
</>
);
}The send button stays active during a stream. Give queue items a different style from messages, so the user can see what is still waiting.
Set whenBusy in the queue option. A string is a short form of { whenBusy }, so queue: "interrupt" also works.
Pass options as the second argument to sendMessage:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function InterruptButton() {
const { sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<button
type="button"
onClick={() =>
sendMessage("Never mind, do this instead", {
whenBusy: "interrupt",
body: { source: "composer" },
})
}
>
Do this instead
</button>
);
}body is extra JSON for this one request. The client merges it into forwardedProps.
The client is busy in three cases:
A send that arrives while the client is busy follows whenBusy. The queue sends its items only after a run ends with success. If the run fails or stops, the queue empties without sending.
| Option | Values | Default | What it does |
|---|---|---|---|
| whenBusy | "queue", "drop", "interrupt" | "queue" | The rule for a send while the client is busy. |
| drain | "fifo", "batch" | "fifo" | "fifo" sends queued items one at a time, in order. "batch" merges the queue into one send. |
| maxSize | a non-negative integer | no limit | How many messages can wait. 0 means that nothing waits. |
| onOverflow | "reject", "drop-oldest" | "reject" | "reject" ignores a send when the queue is full. "drop-oldest" removes the oldest item to make room. |
How "batch" merges the items:
For control over each send, pass a function as queue. It gets the pending message, the reason the client is busy, and the current queue. It returns an action:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function useQueuedChat() {
return useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
queue: ({ pending, busyReason, queued }) => {
// Let the user cut in with a message that starts with "!".
if (typeof pending.content === "string" && pending.content.startsWith("!")) {
return { action: "interrupt" };
}
// Keep at most 3 waiting messages during a stream.
if (busyReason === "streaming" && queued.length >= 3) {
return { action: "drop" };
}
return { action: "queue" };
},
});
}busyReason is one of "streaming", "sendInFlight", or "draining". A strategy function always sends the queue in FIFO order. A whenBusy on one sendMessage call still wins over the function.