Vercel AI SDK v5 React hooks (useChat, useCompletion, useObject) for AI chat interfaces. Use for React/Next.js AI apps or encountering parse stream errors, no response, streaming issues.
bun add ai @ai-sdk/openai # preferred
# or: bun add ai @ai-sdk/openai
// app/chat/page.tsx
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function Chat() {
const { messages, sendMessage, isLoading } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
/>
</form>
</div>
);
}
// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4-turbo'),
messages,
});
return result.toDataStreamResponse();
}
| Reference File | Load When... |
|---|---|
references/use-chat-migration.md | Migrating from v4 to v5, understanding breaking changes |
references/streaming-patterns.md | UI streaming best practices, performance optimization |
references/top-ui-errors.md | Debugging common UI errors, error prevention |
references/nextjs-integration.md | Next.js setup patterns, App vs Pages Router differences |
references/links-to-official-docs.md | Finding official Vercel AI SDK documentation |
references/tool-calling-ui.md | Implementing tool/function calling in chat UI |
references/file-attachments-guide.md | Adding file upload/attachment support to chat |
references/message-persistence.md | Persisting chat history with localStorage |
references/use-completion-full-reference.md | Complete useCompletion API and examples |
references/use-object-full-reference.md | Complete useObject API with Zod schemas |
references/nextjs-app-router-full.md | Full Next.js App Router implementation |
references/nextjs-pages-router-full.md | Full Next.js Pages Router implementation |
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatComponent() {
const { messages, sendMessage, isLoading, error } = useChat({
api: '/api/chat',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ content: input });
setInput('');
};
return (
<div className="flex flex-col h-screen">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
{messages.map(message => (
<div
key={message.id}
className={message.role === 'user' ? 'text-right' : 'text-left'}
>
<div className="inline-block p-2 rounded bg-gray-100">
{message.content}
</div>
</div>
))}
{isLoading && <div className="text-gray-500">AI is thinking...</div>}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isLoading}
className="w-full p-2 border rounded"
/>
</form>
{/* Error */}
{error && <div className="text-red-500 p-4">{error.message}</div>}
</div>
);
}
const {
// Messages
messages, // Message[] - Chat history
setMessages, // (messages: Message[]) => void - Update messages
// Actions
sendMessage, // (message: { content: string }) => void - Send message (v5)
reload, // () => void - Reload last response
stop, // () => void - Stop current generation
// State
isLoading, // boolean - Is AI responding?
error, // Error | undefined - Error if any
// Data
data, // any[] - Custom data from stream
metadata, // object - Response metadata
} = useChat({
// Required
api: '/api/chat', // API endpoint
// Optional
id: 'chat-1', // Chat ID for persistence
initialMessages: [], // Initial messages (controlled mode)
// Callbacks
onFinish: (message, options) => {}, // Called when response completes
onError: (error) => {}, // Called on error
// Configuration
headers: {}, // Custom headers
body: {}, // Additional body data
credentials: 'same-origin', // Fetch credentials
// Streaming
streamProtocol: 'data', // 'data' | 'text' (default: 'data')
});
const { messages, input, handleInputChange, handleSubmit, append } = useChat();
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
const { messages, sendMessage } = useChat();
const [input, setInput] = useState('');
<form onSubmit={(e) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
}}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
input, handleInputChange, handleSubmit no longer existappend() → sendMessage(): New method for sending messagesonResponse removed: Use onFinish insteadinitialMessages → controlled mode: Use messages prop for full controlmaxSteps removed: Handle on server-side onlyreferences/use-chat-migration.md for complete migration guide.message.toolInvocations), file attachments (experimental_attachments), and message persistence (localStorage with id + initialMessages). See official docs for implementation examples.{ completion, complete, isLoading }. Call complete(prompt) to generate. API route uses streamText().{ object, submit, isLoading } where object is Partial<T> that updates progressively. API route uses streamObject().references/nextjs-integration.md for complete implementation code including:toDataStreamResponse() vs pipeDataStreamToResponse()references/top-ui-errors.md for complete documentation. Quick reference:SyntaxError: Unexpected token in JSON at position X// ✅ CORRECT
return result.toDataStreamResponse();
// ❌ WRONG
return new Response(result.textStream);
// App Router - use toDataStreamResponse()
export async function POST(req: Request) {
const result = streamText({ /* ... */ });
return result.toDataStreamResponse(); // ✅
}
// Pages Router - use pipeDataStreamToResponse()
export default async function handler(req, res) {
const result = streamText({ /* ... */ });
return result.pipeDataStreamToResponse(res); // ✅
}
body option captured at first render only.// ❌ WRONG - body captured once
const { userId } = useUser();
const { messages } = useChat({
body: { userId }, // Stale!
});
// ✅ CORRECT - use controlled mode
const { userId } = useUser();
const { messages, sendMessage } = useChat();
sendMessage({
content: input,
data: { userId }, // Fresh on each send
});
// ❌ WRONG
useEffect(() => {
saveMessages(messages);
}, [messages, saveMessages]); // saveMessages triggers re-render!
// ✅ CORRECT
useEffect(() => {
saveMessages(messages);
}, [messages]); // Only depend on messages
references/top-ui-errors.md for 7 more common errors.// ✅ GOOD - Streaming (shows tokens as they arrive)
const { messages } = useChat({ api: '/api/chat' });
// ❌ BAD - Non-streaming (user waits for full response)
const response = await fetch('/api/chat', { method: 'POST' });
{isLoading && <div>AI is typing...</div>}
{isLoading && <button onClick={stop}>Stop</button>}
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
<input disabled={isLoading} />
references/streaming-patterns.md for comprehensive best practices.{
"dependencies": {
"ai": "^5.0.116",
"@ai-sdk/openai": "^2.0.88",
"react": "^18.2.0",
"zod": "^3.23.8",
"isomorphic-dompurify": "^2.16.0"
}
}
{
"dependencies": {
"next": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
templates/:references/ for: