Loading...
Loading...
Build React chat interfaces with Vercel AI SDK v6. Covers useChat/useCompletion/useObject hooks, message parts structure, tool approval workflows, and 18 UI error solutions. Prevents documented issues with React Strict Mode, concurrent requests, stale closures, and tool approval edge cases. Use when: implementing AI chat UIs, migrating v5→v6, troubleshooting "useChat failed to parse stream", "stale body values", "React maximum update depth", "Cannot read properties of undefined (reading 'state')", or tool approval workflow errors.
npx skill4agent add jezweb/claude-skills ai-sdk-ui.parts.content// ❌ v5 (OLD)
{messages.map(m => (
<div key={m.id}>{m.content}</div>
))}
// ✅ v6 (NEW)
{messages.map(m => (
<div key={m.id}>
{m.parts.map((part, i) => {
if (part.type === 'text') return <span key={i}>{part.text}</span>;
if (part.type === 'tool-invocation') return <ToolCall key={i} tool={part} />;
if (part.type === 'file') return <FilePreview key={i} file={part} />;
return null;
})}
</div>
))}text.texttool-invocation.toolName.args.resultfile.mimeType.datareasoningsourceInferAgentUIMessage<typeof agent>import { useChat } from '@ai-sdk/react';
import type { InferAgentUIMessage } from 'ai';
import { myAgent } from './agent';
export default function AgentChat() {
const { messages, sendMessage } = useChat<InferAgentUIMessage<typeof myAgent>>({
api: '/api/chat',
});
// messages are now type-checked against agent schema
}import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
api: '/api/chat',
});
const handleApprove = (toolCallId: string) => {
addToolApprovalResponse({
toolCallId,
approved: true, // or false to deny
});
};
return (
<div>
{messages.map(message => (
<div key={message.id}>
{message.toolInvocations?.map(tool => (
tool.state === 'awaiting-approval' && (
<div key={tool.toolCallId}>
<p>Approve tool call: {tool.toolName}?</p>
<button onClick={() => handleApprove(tool.toolCallId)}>
Approve
</button>
<button onClick={() => addToolApprovalResponse({
toolCallId: tool.toolCallId,
approved: false
})}>
Deny
</button>
</div>
)
))}
</div>
))}
</div>
);
}import { useChat, lastAssistantMessageIsCompleteWithApprovalResponses } from '@ai-sdk/react';
export default function AutoSubmitChat() {
const { messages, sendMessage } = useChat({
api: '/api/chat',
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
// Automatically resubmit after all approval responses provided
});
}useObjectimport { useChat } from '@ai-sdk/react';
import { z } from 'zod';
const schema = z.object({
summary: z.string(),
sentiment: z.enum(['positive', 'neutral', 'negative']),
});
export default function StructuredChat() {
const { messages, sendMessage } = useChat({
api: '/api/chat',
// Server can now stream structured output with chat messages
});
}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>inputhandleInputChangehandleSubmitappend()sendMessage()onResponseonFinishinitialMessagesmessagesmaxStepsreferences/use-chat-migration.md⚠️ Deprecation Notice:is deprecated as of AI SDK v5. OpenAI Assistants API v2 will sunset on August 26, 2026. For new projects, useuseAssistantwith custom backend logic instead. See the openai-assistants skill for migration guidance.useChat
import { useAssistant } from '@ai-sdk/react';'use client';
import { useAssistant } from '@ai-sdk/react';
import { useState, FormEvent } from 'react';
export default function AssistantChat() {
const { messages, sendMessage, isLoading, error } = useAssistant({
api: '/api/assistant',
});
const [input, setInput] = useState('');
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
sendMessage({ content: input });
setInput('');
};
return (
<div>
{messages.map(m => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
/>
</form>
{error && <div>{error.message}</div>}
</div>
);
}references/top-ui-errors.mdSyntaxError: 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// ❌ 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 messagesreferences/top-ui-errors.md// ✅ 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.mduseChatuseCompletion'use client';
import { useChat } from '@ai-sdk/react';
import { useEffect } from 'react';
export default function Chat() {
const { messages, sendMessage, resumeStream } = useChat({
api: '/api/chat',
resume: true,
});
useEffect(() => {
// ❌ Triggers twice in strict mode → two concurrent streams
sendMessage({ content: 'Hello' });
// or
resumeStream();
}, []);
}// ✅ Use ref to track execution
import { useRef } from 'react';
const hasSentRef = useRef(false);
useEffect(() => {
if (hasSentRef.current) return;
hasSentRef.current = true;
sendMessage({ content: 'Hello' });
}, []);
// For resumeStream specifically:
const hasResumedRef = useRef(false);
useEffect(() => {
if (!autoResume || hasResumedRef.current || status === 'streaming') return;
hasResumedRef.current = true;
resumeStream();
}, [autoResume, resumeStream, status]);{
"dependencies": {
"ai": "^6.0.8",
"@ai-sdk/react": "^3.0.6",
"@ai-sdk/openai": "^3.0.2",
"react": "^18.3.0",
"zod": "^3.24.2"
}
}{
"dependencies": {
"ai": "^5.0.99",
"@ai-sdk/react": "^1.0.0",
"@ai-sdk/openai": "^2.0.68"
}
}templates/references/