Command Palette

Search for a command to run...

Hectal
PHASE 5Intermediate ~30 min· topic 3 of 9

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 as useChat, that keeps state for a component.
Status
ready, submitted, streaming or error: 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 ready and clears error, without sending a request.
Transport
The object that does the network part. DefaultChatTransport posts 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().

The status diagramdiagram
Rendering diagram…

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. 1

    Remove the guard from the form

    In the first example change if (!text || busy) return; to if (!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. 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 between regenerate() and sending the same text again. (The lines after it then report ready, because the error is already gone.)

  3. 3

    Show a retry button only after an error

    In the first example, add {status === "error" && <button onClick={() => regenerate()}>Retry</button>} (take regenerate from 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

A complete chat: statuses on screen, and stop

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

Press Run to see the page.

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: false
Two sends at once: what the server receives

In 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

Press Run to see the page.

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: ready
Four ways it fails, and how to get out

A 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

Press Run to see the page.

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."
Ids, the chat id and regenerate

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

Press Run to see the page.

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-chat

Break 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.

terminal
$ The console, second half
── what you'll see ──
--- with a guard
(server) request 3 has 5 messages; roles: uauau
(server) request 4 has 7 messages; roles: uauauau

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.

  • ▸

    sendMessage returns a promise that settles when the answer has finished, so you can await it 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 call onError.

  • ▸

    Pass onError, onFinish and onData as options for side effects such as logging, analytics and toasts. Do not copy the status into your own state in an effect: read status and error directly (this is also a rule of the React course).

  • ▸

    submitted ends at the first chunk, not the first word. In a run with a 150 ms wait for the first word, the status became streaming almost 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 is submitted, or when it is streaming and 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 Chat object once (new Chat({...}) from @ai-sdk/react) and pass it as useChat({ chat }). Use this when the message list and the input live far apart in the tree.

Remember this

  1. 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. useChat is 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. 2

    What you call: const { messages, sendMessage, status, stop, regenerate, error, clearError, setMessages } = useChat({ transport }). The transport says where to send: new DefaultChatTransport({ api: "/api/chat" }) (the default URL is already /api/chat). messages is an array of UI messages, each { id, role, parts }. You render parts (Topic 5.4). sendMessage({ text }) adds the user's message and starts the request.

  3. 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. 4

    Stop keeps what arrived. stop() aborts the request and moves to ready. The assistant message stays in messages with the text that had arrived; its text part stays in the streaming state, so mark it as stopped in your UI (Topic 3.2). Stopping does not delete anything.

  5. 5

    Errors. On a failure the status becomes error and error holds an Error. The user message you sent stays in messages (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 to ready without sending anything, and regenerate() 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.message is the response body as text and error.statusCode is the status; for a failure inside the stream, error.message is the masked 'An error occurred.' (Topic 5.6 covers handling both).

  6. 6

    **What useChat does not do.** It does not own your input box: keep the text in your own useState. It does not stop a second sendMessage while 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 unless status is ready or error, 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. 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. Use message.id as the React key, never the array index (Topic 4.1). The chat itself has an id too, sent to your route with every request, and you can pass your own generateId for predictable ids in tests. A regenerated answer is a new message with a new id.

Explain it without notes

01

Name the four useChat statuses and what the screen should show in each.

02

What happens to the messages when the user presses stop, and when a request fails?

03

Why can two quick sendMessage calls corrupt a conversation, and how do you prevent it?

04

What is the difference between regenerate(), sending the same text again and clearError()?

Practice

01

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().

02

Add a 'New chat' button that empties the conversation. What must you do first if an answer is streaming?

Trade-offs

  • ↔

    useChat removes 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 messages after 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.id as a key and explain what changes after regenerate().