The useChat hook makes it effortless to create a conversational user interface for your chatbot application. It enables the streaming of chat messages from your AI provider, manages the chat state, and updates the UI automatically as new messages arrive.
To summarize, the useChat hook provides the following features:
Message Streaming: All the messages from the AI provider are streamed to the chat UI in real-time.
Managed States: The hook manages the states for input, messages, status, error and more for you.
Seamless Integration: Easily integrate your chat AI into any design or layout with minimal effort.
In this guide, you will learn how to use the useChat hook to create a chatbot application with real-time message streaming.
Check out our chatbot with tools guide to learn how to use tools in your chatbot.
Let's start with the following example first.
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
UIMessage,
} from 'ai';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "anthropic/claude-sonnet-5.5",
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
});
}
The UI messages have a new parts property that contains the message parts.
We recommend rendering the messages using the parts property instead of the
content property. The parts property supports different message types,
including text, tool invocation, and tool result, and allows for more flexible
and complex chat UIs.
In the Page component, the useChat hook will request to your AI provider endpoint whenever the user sends a message using sendMessage.
The messages are then streamed back in real-time and displayed in the chat UI.
This enables a seamless chat experience where the user can see the AI response as soon as it is available,
without having to wait for the entire response to be received.
Similarly, the error state reflects the error object thrown during the fetch request.
It can be used to display an error message, disable the submit button, or show a retry button:
We recommend showing a generic error message to the user, such as "Something
went wrong." This is a good practice to avoid leaking information from the
server.
Sometimes, you may want to directly modify some existing messages. For example, a delete button can be added to each message to allow users to remove them from the chat history.
The setMessages function can help you achieve these tasks:
It's also a common use case to abort the response message while it's still streaming back from the AI provider. You can do this by calling the stop function returned by the useChat hook.
const { stop, status } = useChat()return <> <button onClick={stop} disabled={!(status === 'streaming' || status === 'submitted')}>Stop</button> ...
When the user clicks the "Stop" button, the fetch request will be aborted. This avoids consuming unnecessary resources and improves the UX of your chatbot application.
Similarly, you can also request the AI provider to reprocess the last message by calling the regenerate function returned by the useChat hook:
By default, useChat publishes message updates as each chunk is processed.
React may batch multiple updates into a single render.
React and Vue applications can throttle reactive message updates with the throttle option.
Stream processing and event callbacks remain immediate, and the latest messages are published before the chat enters a terminal ready or error status.
useChat provides optional event callbacks that you can use to handle different stages of the chatbot lifecycle:
onFinish: Called when the assistant response is completed. The event includes the response message, all messages, and flags for abort, disconnect, and errors.
onError: Called when an error occurs during the fetch request.
onData: Called whenever a data part is received.
These callbacks can be used to trigger additional actions, such as logging, analytics, or custom UI updates.
import { UIMessage } from 'ai';const { /* ... */} = useChat({ onFinish: ({ message, messages, isAbort, isDisconnect, isError }) => { // use information to e.g. update other UI states }, onError: error => { console.error('An error occurred:', error); }, onData: data => { console.log('Received data part from server:', data); },});
It's worth noting that you can abort the processing by throwing an error in the onData callback. This will trigger the onError callback and stop the message from being appended to the chat UI. This can be useful for handling unexpected responses from the AI provider.
By default, the useChat hook sends an HTTP POST request to the /api/chat endpoint with the message list as the request body. You can customize the request in two ways:
You can also provide functions that return configuration values. This is useful for authentication tokens that need to be refreshed, or for configuration that depends on runtime conditions:
For component state that changes over time, use useRef to store the current
value and reference ref.current in your configuration function, or prefer
request-level options (see next section) for better reliability.
Recommended: Use request-level options for better flexibility and control.
Request-level options take precedence over hook-level options and allow you to
customize each request individually.
// Pass options as the second parameter to sendMessagesendMessage( { text: input }, { headers: { Authorization: 'Bearer token123', 'X-Custom-Header': 'custom-value', }, body: { temperature: 0.7, max_tokens: 100, user_id: '123', }, metadata: { userId: 'user123', sessionId: 'session456', }, },);
The request-level options are merged with hook-level options, with request-level options taking precedence. On your server side, you can handle the request with this additional information.
You can configure custom body fields on a per-request basis using the second parameter of the sendMessage function.
This is useful if you want to pass in additional information to your backend that is not part of the message list.
You can retrieve these custom fields on your server side by destructuring the request body:
app/api/chat/route.ts
export async function POST(req: Request) { // Extract additional information ("customKey") from the body of the request: const { messages, customKey }: { messages: UIMessage[]; customKey: string } = await req.json(); //...}
The DirectChatTransport invokes the agent's stream() method directly, converting UI messages to model messages and streaming the response back as UI message chunks.
By default, the error message is masked for security reasons.
The default error message is "An error occurred."
Tool execution errors marked providerExecuted: true are an exception. They
bypass the UI stream's onError callback: strings are forwarded unchanged, and
other error values are serialized with JSON.stringify. This preserves provider
error codes when chat history is converted back into model messages, as well as
error messages from harness runtimes. Stream errors and invalid tool calls still
use onError, even for provider-executed tools. Do not rely on this callback to
redact provider-executed tool execution errors before they reach the client.
You can forward error messages or send your own error message by providing an onError function:
Track token consumption and resource usage with message metadata:
Define a custom metadata type with usage fields (optional, for type safety)
Attach usage data using messageMetadata in your response
Display usage metrics in your UI components
Usage data is attached as metadata to messages and becomes available once the model completes its response generation.
import { openai } from '@ai-sdk/openai';
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
UIMessage,
type LanguageModelUsage,
} from 'ai';
// Create a new metadata type (optional for type-safety)
type MyMetadata = {
totalUsage: LanguageModelUsage;
};
// Create a new custom message type with your own metadata
export type MyUIMessage = UIMessage<MyMetadata>;
export async function POST(req: Request) {
const { messages }: { messages: MyUIMessage[] } = await req.json();
const result = streamText({
model: "anthropic/claude-sonnet-5.5",
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({
stream: result.stream,
originalMessages: messages,
messageMetadata: ({ part }) => {
// Send total usage when generation is finished
if (part.type === 'finish') {
return { totalUsage: part.totalUsage };
}
},
}),
});
}
Then, on the client, you can access the message-level metadata.
'use client';import { useChat } from '@ai-sdk/react';import type { MyUIMessage } from './api/chat/route';import { DefaultChatTransport } from 'ai';export default function Chat() { // Use custom message type defined on the server (optional for type-safety) const { messages } = useChat<MyUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} {/* Render usage via metadata */} {m.metadata?.totalUsage && ( <div>Total usage: {m.metadata?.totalUsage.totalTokens} tokens</div> )} </div> ))} </div> );}
You can also access your metadata from the onFinish callback of useChat:
'use client';import { useChat } from '@ai-sdk/react';import type { MyUIMessage } from './api/chat/route';import { DefaultChatTransport } from 'ai';export default function Chat() { // Use custom message type defined on the server (optional for type-safety) const { messages } = useChat<MyUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), onFinish: ({ message }) => { // Access message metadata via onFinish callback console.log(message.metadata?.totalUsage); }, });}
Some models such as DeepSeek deepseek-r1
and Anthropic claude-sonnet-5-5 support reasoning tokens.
These tokens are typically sent before the message content.
You can forward them to the client with the sendReasoning option:
Some models may also produce files as part of reasoning (e.g. images).
These are available as reasoning-file parts (ReasoningFileUIPart) with
mediaType and url properties, similar to regular file parts.
Some providers such as Perplexity and
Google include sources in the response.
You can forward sources to the client with the sendSources option. This example uses the Perplexity Agent API's fast preset to search the web. Install @ai-sdk/perplexity and set PERPLEXITY_API_KEY in your environment:
On the client side, you can access source parts of the message object.
There are two types of sources: source-url for web pages and source-document for documents.
Here is an example that renders both types of sources:
Some models such as Google gemini-3.1-flash-image-preview support image generation.
When images are generated, they are exposed as files to the client.
On the client side, you can access file parts of the message object
and render them as images.
The useChat hook supports sending file attachments along with a message as well as rendering them on the client. This can be useful for building applications that involve sending images, files, or other media content to the AI provider.
There are two ways to send files with a message: using a FileList object from file inputs or using an array of file objects.
By using FileList, you can send multiple files as attachments along with a message using the file input element. The useChat hook will automatically convert them into data URLs and send them to the AI provider.
Currently, only image/* and text/* content types get automatically
converted into multi-modal content
parts. You will need to handle other
content types manually.
The InferUITool type helper infers the input and output types of a single tool for use in UI messages:
import { InferUITool } from 'ai';import { z } from 'zod';const weatherTool = { description: 'Get the current weather', inputSchema: z.object({ location: z.string().describe('The city and state'), }), execute: async ({ location }) => { return `The weather in ${location} is sunny.`; },};// Infer the types from the tooltype WeatherUITool = InferUITool<typeof weatherTool>;// This creates a type with:// {// input: { location: string };// output: string;// }