Topic 5.3
useChat
In one line
useChat is the browser half of the AI SDK. It sends your messages to the route, rebuilds the streamed chunks into message objects as they arrive, and tells you which of four statuses the chat is in (ready, submitted, streaming, error). It gives you stop, regenerate and clearError, but not an input box and not a double-click guard.
Think of it like this
A restaurant order ticket and the status board above the counter. The ticket lists what was ordered; the board shows 'received', 'cooking', 'ready', or 'problem with your order'. The waiter can cancel a ticket mid-cooking and the half-made dish stays on the counter. **useChat is the ticket plus the board: it holds the conversation and the status, and you decide how to draw them.**
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Hook
- A React function whose name starts with
use, such asuseChat, that keeps state for a component. - Status
ready,submitted,streamingorerror: the single value that says what the chat is doing.- sendMessage
- Adds a user message and sends the whole conversation to the route. It returns a promise that settles when the answer has finished.
- stop
- Aborts the running request. The text that arrived stays in
messages. - regenerate
- Throws away the last assistant message and asks for a new one to the same question.
- clearError
- Sets the status back to
readyand clearserror, without sending a request. - Transport
- The object that does the network part.
DefaultChatTransportposts JSON and reads the UI message stream. - Chat id
- An id for the conversation, sent with every request so your server can tell chats apart.
- Optimistic message
- A user message that shows on screen the moment you send it, before the server has answered.
Step by step
01The status diagram
Learn this picture: every behaviour of useChat is a move on it. submitted lasts until the first chunk of the stream arrives, which can be a moment before the first word (Topic 2.1). From error you can send again, regenerate() or clearError().
02A complete chat in one component
The first live example is the whole screen: a message list, a thinking line for submitted, a stop button while busy, and a form whose button is disabled unless the chat is ready. A scripted visitor asks two questions and presses stop during the second answer.
Watch the console. Each status: line is one move on the diagram. After the stop, the second answer is shorter than the full reply, and the form is usable again.
03Two sends at once
The second example calls sendMessage twice, 150 ms apart, with no guard. The server log shows that request 2 contained three messages: the first question, the half-written first answer, and the second question. The model would answer the second question with a broken answer in its history.
Then the same two sends go through a one-line guard (if (status !== "ready") return) and the second is ignored.
04Four ways a request fails
The third example sends to a route that fails in four different ways: a 429 from the provider before the first token, a 401 from your own route, a broken connection (nothing answers the URL), and a stream that dies after two words. For each it prints the status, how the message reads, and what stayed in messages.
Then it recovers: clearError() returns to ready, and regenerate() produces a complete answer.
05Ids and the request your route receives
The fourth example gives useChat an id and a generateId, so the ids are predictable. The server log shows the id and a trigger in each request body: submit-message for a send and regenerate-message for a regenerate. After regenerate() the assistant message has a new id.
Try it yourself
- 1
Remove the guard from the form
In the first example change
if (!text || busy) return;toif (!text) return;and replace the stop/send ternary with a plain Send button that is never disabled. Press Run and watch the server-less console while the visitor works.With a button that stays enabled, a quick second click sends while the first answer is streaming, as in the second example. The disabled button is the simplest and best guard.
- 2
Regenerate after a broken stream
In the third example, add this line just before
console.log("--- recovering from the last error");:mode = "ok"; await chat.regenerate(); await wait(50); console.log("regenerated:", chat.status, chat.messages.map((x) => x.role[0] + "=" + JSON.stringify(text(x))).join(" "));The line prints
regenerated: ready u="Where is my order?" a="Orders ship in 1 to 2 working days.": the broken half answer is replaced by the complete answer, and the question is not repeated. That is the difference betweenregenerate()and sending the same text again. (The lines after it then reportready, because the error is already gone.) - 3
Show a retry button only after an error
In the first example, add
{status === "error" && <button onClick={() => regenerate()}>Retry</button>}(takeregeneratefrom the hook).Change the model to
fakeModel({ fail: { status: 500 } })and ask a question. The Retry button appears. Change it back and press Retry:regenerate()sends the same question again.
Code & diagrams
The visitor types, sends, and presses Stop for you. useChat gave you messages, status and stop; the thinking line, the stop button and the disabled Send are your screen. After the run, type your own question.
Preview
Console
Nothing logged yet.Expected console output
status: ready
status: submitted
status: streaming
status: ready
answer 1 complete: true
status: submitted
status: streaming
stop pressed
status: ready
answer 2 complete: falseIn request 2 the roles are 'uau': the first question, the half-written first answer, the second question. The SDK did not stop you. Roles: u is user, a is assistant.
Preview
Console
Nothing logged yet.Expected console output
--- without a guard
(server) request 1 has 1 messages; roles: u
(server) request 2 has 3 messages; roles: uau
messages now: uaua | status: ready
--- with a guard
(server) request 3 has 5 messages; roles: uauau
ignored (status is streaming): fourth
messages now: uauaua | status: readyA failure status such as 401 puts the response body in error.message and its code in statusCode. A failure inside the stream always reads 'An error occurred.' (the server masked it). A request that never gets an answer is a TypeError. In every case the user message stays in messages.
Preview
Console
Nothing logged yet.Expected console output
limit: status error | status code undefined | message "An error occurred." | messages kept: u="Where is my order?"
signedout: status error | status code 401 | message "{\"error\":\"Please sign in.\"}" | messages kept: u="Where is my order?"
offline: status error | status code undefined | message "Failed to fetch" | messages kept: u="Where is my order?"
cut: status error | status code undefined | message "An error occurred." | messages kept: u="Where is my order?" a="Orders ship in one"
--- recovering from the last error
before clearError: error
after clearError: ready | the assistant text is still there: "Orders ship in one"
fresh chat after the fix: ready "Orders ship in 1 to 2 working days."Your own generateId is mainly useful in tests and when you need ids that match your database. The regenerated answer is a different message, so anything you stored against the old id (a thumbs-up, a note) must be dropped or moved.
Preview
Console
Nothing logged yet.Expected console output
(server) chat id: support-chat | trigger: submit-message | messages: 1
after send: u=msg-1 a=msg-2
(server) chat id: support-chat | trigger: regenerate-message | messages: 1
after regenerate: u=msg-1 a=msg-3
(server) chat id: support-chat | trigger: submit-message | messages: 3
after a new send: u=msg-1 a=msg-3 u=msg-4 a=msg-5 | chat id: support-chatBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Send without checking the status
In the second example, delete the guard line if (chat.status !== "ready") return console.log(…); inside guardedSend, and run it.
Myth vs fact
Myth
useChat stops me from sending while an answer is streaming.
Fact
It does not. A second sendMessage goes straight out, and its request includes the half-written answer. Guard on status yourself.
Myth
After an error the chat is unusable until it is reset.
Fact
Sending again, regenerate() and clearError() all work from the error status. The messages are kept.
Myth
useChat keeps my chat after a reload.
Fact
Messages live in memory. Persistence (Topic 5.8) is your job: save in the route or the client and pass them back as the messages option.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
sendMessagereturns a promise that settles when the answer has finished, so you canawaitit in tests and scripts. In an event handler you usually do not await it, and errors do not reject it: they set the status and callonError. - ▸
Pass
onError,onFinishandonDataas options for side effects such as logging, analytics and toasts. Do not copy the status into your own state in an effect: readstatusanderrordirectly (this is also a rule of the React course). - ▸
submittedends at the first chunk, not the first word. In a run with a 150 ms wait for the first word, the status becamestreamingalmost at once, with an assistant message whose text part was still empty (""). If you want a 'thinking' line until text really appears, show it when the status issubmitted, or when it isstreamingand the last assistant message has no text yet. - ▸
For very fast streams,
useChat({ throttle: 50 })batches message updates to at most one every 50 ms. Use it when a long Markdown answer makes rendering expensive (Phase 6), not by default. - ▸
Several components can share one conversation: create a
Chatobject once (new Chat({...})from@ai-sdk/react) and pass it asuseChat({ chat }). Use this when the message list and the input live far apart in the tree.
Remember this
- 1
The problem this solves: In Phase 4 you wrote a reducer for the messages, with actions for send, token, done, error and stop. It worked, and it was a lot of code that every chat app repeats.
useChatis that reducer, the read loop and the abort logic, written once and tested. This topic shows what it gives you, the exact meaning of each status, and the places where it still expects you to do the careful part. - 2
What you call:
const { messages, sendMessage, status, stop, regenerate, error, clearError, setMessages } = useChat({ transport }). Thetransportsays where to send:new DefaultChatTransport({ api: "/api/chat" })(the default URL is already/api/chat).messagesis an array of UI messages, each{ id, role, parts }. You renderparts(Topic 5.4).sendMessage({ text })adds the user's message and starts the request. - 3
Four statuses.
ready: nothing is running; you may send.submitted: the request was sent and no answer has started; show a thinking line.streaming: chunks are arriving and the assistant message grows; show a cursor and a stop button.error: the request or the stream failed; show the error and a retry. These are the four states you drew in Topic 0.4, now named for you. The status is a single value, so impossible combinations cannot happen. - 4
Stop keeps what arrived.
stop()aborts the request and moves toready. The assistant message stays inmessageswith the text that had arrived; its text part stays in thestreamingstate, so mark it as stopped in your UI (Topic 3.2). Stopping does not delete anything. - 5
Errors. On a failure the status becomes
erroranderrorholds anError. The user message you sent stays inmessages(the SDK does not remove it), so the next send puts a second user message after it. If the failure was inside the stream, the partial assistant text is kept.clearError()returns the status toreadywithout sending anything, andregenerate()asks for a new answer to the last user message, replacing the last assistant message. For a failure status such as 401 or 502,error.messageis the response body as text anderror.statusCodeis the status; for a failure inside the stream,error.messageis the masked 'An error occurred.' (Topic 5.6 covers handling both). - 6
**What
useChatdoes not do.** It does not own your input box: keep the text in your ownuseState. It does not stop a secondsendMessagewhile one is running: both go out at once, and the second request carries the half-written assistant message of the first (the second example shows it). So disable the send button unlessstatusisreadyorerror, and guard the handler too. The hook also keeps messages in memory only: reload the page and they are gone until you restore them (Topic 5.8). - 7
Ids. Every message has an
id. The user message gets one when you send it, and the assistant message gets one when its stream starts. Usemessage.idas the Reactkey, never the array index (Topic 4.1). The chat itself has anidtoo, sent to your route with every request, and you can pass your owngenerateIdfor predictable ids in tests. A regenerated answer is a new message with a new id.
Explain it without notes
Name the four useChat statuses and what the screen should show in each.
What happens to the messages when the user presses stop, and when a request fails?
Why can two quick sendMessage calls corrupt a conversation, and how do you prevent it?
What is the difference between regenerate(), sending the same text again and clearError()?
Practice
Add a visible error line #error (with role="alert") to the first example. Show 'Mitra is busy. Please try again.' for any error and a Retry button that calls regenerate().
Add a 'New chat' button that empties the conversation. What must you do first if an answer is streaming?
Trade-offs
- ↔
useChatremoves the whole reducer, read loop and abort plumbing, but its state lives inside the hook. If you need a different shape (for example several parallel answers per question, or a custom retry policy), you are fighting the hook, and a lower-level approach may fit better. - ↔
Optimistic user messages feel instant, but a failed request leaves a question with no answer in the list. Decide whether to leave it (and offer retry) or remove it (and restore the text), and apply the same choice everywhere.
Done when you can
I can draw the status diagram and name every transition.
I can render messages, a thinking line, a stop button and an error with useChat.
I can explain what stays in
messagesafter stop and after each kind of error.I can prevent a double send with a disabled button and a handler guard.
I can use
message.idas a key and explain what changes afterregenerate().