useChat

useChat comes from @ai-sdk/react (not the ai package). It POSTs to /api/chat, reassembles the UI Message stream into messages, and tracks status / error. You still write the layout in React. The hook does not own the input box—keep that in useState.

The default transport hits POST /api/chat. For a custom URL, headers, or body, pass DefaultChatTransport from ai.


Minimal page

'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';

export default function Page() {
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({ api: '/api/chat' }),
  });
  const [input, setInput] = useState('');

  return (
    <>
      {messages.map((message) => (
        <div key={message.id}>
          {message.role === 'user' ? 'User: ' : 'AI: '}
          {message.parts.map((part, index) =>
            part.type === 'text' ? <span key={index}>{part.text}</span> : null,
          )}
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (!input.trim()) return;
          sendMessage({ text: input });
          setInput('');
        }}
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          disabled={status !== 'ready'}
        />
        <button type="submit" disabled={status !== 'ready'}>
          Send
        </button>
      </form>
    </>
  );
}

The server must return a UI Message Stream (see createUIMessageStreamResponse in Quick start). If the protocols do not match, the UI spins forever or errors.


Message shape: parts

Each UIMessage has id, role, and parts. parts is an ordered array that may include:

part.typeMeaning
textPlain text in part.text
tool-weatherThe tool named weather (pattern: tool-{toolName})
Reasoning / files / custom data-*See the official Chatbot guide

Do not treat legacy message.content as the main path. Tool results and reasoning tokens live on parts.


status and controls

statusMeaningUI
submittedSent; stream not startedSpinner
streamingChunks arrivingStop button
readyCan send againEnable the input
errorRequest failedGeneric error + Retry
const { status, stop, error, regenerate } = useChat();

{(status === 'submitted' || status === 'streaming') && (
  <button type="button" onClick={() => stop()}>Stop</button>
)}
{error && (
  <button type="button" onClick={() => regenerate()}>Retry</button>
)}

stop() aborts the in-flight fetch. regenerate() asks the model to rewrite the last assistant message. Show a generic error to users; do not leak server stacks into the browser.


History, throttle, callbacks

messages / setMessages behave like useState. Delete by id:

const { messages, setMessages } = useChat();
setMessages(messages.filter((m) => m.id !== idToDelete));

On React you can set throttle: 50 (ms) so every token does not force a render.

Optional callbacks: onFinish (assistant done), onError, onData (data parts). Throwing inside onData aborts and runs onError.


Request configuration

Hook-level (every request):

useChat({
  transport: new DefaultChatTransport({
    api: '/api/custom-chat',
    headers: { Authorization: `Bearer ${token}` },
    body: { user_id: '123' },
  }),
});

headers / body may be functions so you can refresh tokens. Per-request options override hook-level config. Never hard-code secrets in client code; read tokens from the signed-in session.


Sibling hooks (awareness)

HookUse for
useChatMulti-turn chat (this tutorial’s main path)
useCompletionOne-shot completion
useObjectPartial JSON from streamText + Output.object

Production chat = Core + UI. Do not default to experimental RSC; see RSC.


Next

评论