AI SDK UI - Frontend React Hooks
Frontend React hooks for AI-powered user interfaces with Vercel AI SDK v5.
Version: AI SDK v5.0.76+ (Stable) Framework: React 18+, Next.js 14+ Last Updated: 2025-10-22
Quick Start (5 Minutes)
Installation
npm install ai @ai-sdk/openai
Basic Chat Component (v5)
// 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>
);
}
API Route (Next.js App Router)
// 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();
}
Result: A functional chat interface with streaming AI responses in ~10 lines of frontend code.
useChat Hook - Complete Reference
Basic Usage (v5 Pattern)
'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>
);
}
Full API Reference
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')
});
v4 → v5 Breaking Changes
CRITICAL: useChat no longer manages input state in v5!
v4 (OLD - DON'T USE):
const { messages, input, handleInputChange, handleSubmit, append } = useChat();
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
v5 (NEW - CORRECT):
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>
Summary of v5 Changes:
- Input management removed:
input,handleInputChange,handleSubmitno longer exist append()→sendMessage(): New method for sending messagesonResponseremoved: UseonFinishinsteadinitialMessages→ controlled mode: Usemessagesprop for full controlmaxStepsremoved: Handle on server-side only
See references/use-chat-migration.md for complete migration guide.
Tool Calling in UI
When your API uses tools, useChat automatically handles tool invocations in the message stream:
'use client';
import { useChat } from 'ai/react';
export default function ChatWithTools() {
const { messages } = useChat({ api: '/api/chat' });
return (
<div>
{messages.map(message => (
<div key={message.id}>
{/* Text content */}
{message.content && <p>{message.content}</p>}
{/* Tool invocations */}
{message.toolInvocations?.map((tool, idx) => (
<div key={idx} className="bg-blue-50 p-2 rounded my-2">
<div className="font-bold">Tool: {tool.toolName}</div>
<div className="text-sm">
<strong>Args:</strong> {JSON.stringify(tool.args, null, 2)}
</div>
{tool.result && (
<div className="text-sm">
<strong>Result:</strong> {JSON.stringify(tool.result, null, 2)}
</div>
)}
</div>
))}
</div>
))}
</div>
);
}
File Attachments
Upload files (images, PDFs, etc.) alongside messages:
'use client';
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';
export default function ChatWithAttachments() {
const { messages, sendMessage, isLoading } = useChat({ api: '/api/chat' });
const [input, setInput] = useState('');
const [files, setFiles] = useState<FileList | null>(null);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({
content: input,
experimental_attachments: files
? Array.from(files).map(file => ({
name: file.name,
contentType: file.type,
url: URL.createObjectURL(file),
}))
: undefined,
});
setInput('');
setFiles(null);
};
return (
<div>
{/* Messages */}
{messages.map(m => (
<div key={m.id}>
{m.content}
{m.experimental_attachments?.map((att, idx) => (
<div key={idx}>
<img src={att.url} alt={att.name} />
</div>
))}
</div>
))}
{/* Input */}
<form onSubmit={handleSubmit}>
<input
type="file"
multiple
onChange={(e) => setFiles(e.target.files)}
accept="image/*"
/>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" disabled={isLoading}>Send</button>