0
# useChat Hook
1
2
Multi-turn conversational AI with message history, streaming responses, and tool call support.
3
4
## API
5
6
```typescript { .api }
7
function useChat<UI_MESSAGE extends UIMessage = UIMessage>(
8
options?: UseChatOptions<UI_MESSAGE>
9
): UseChatHelpers<UI_MESSAGE>;
10
11
interface UseChatHelpers<UI_MESSAGE extends UIMessage> {
12
readonly id: string;
13
messages: UI_MESSAGE[];
14
setMessages: (messages: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[])) => void;
15
sendMessage: (
16
message?: CreateUIMessage<UI_MESSAGE> | { text: string; files?: FileList | FileUIPart[] },
17
options?: ChatRequestOptions
18
) => Promise<void>;
19
regenerate: (options?: { messageId?: string } & ChatRequestOptions) => Promise<void>;
20
stop: () => Promise<void>;
21
resumeStream: (options?: ChatRequestOptions) => Promise<void>;
22
addToolResult: <TOOL extends keyof InferUIMessageTools<UI_MESSAGE>>(
23
options: { tool: TOOL; toolCallId: string; output: InferUIMessageTools<UI_MESSAGE>[TOOL]['output'] }
24
| { state: 'output-error'; tool: TOOL; toolCallId: string; errorText: string }
25
) => Promise<void>;
26
status: 'submitted' | 'streaming' | 'ready' | 'error';
27
error: Error | undefined;
28
clearError: () => void;
29
}
30
```
31
32
## Basic Usage
33
34
```typescript
35
import { useChat } from '@ai-sdk/react';
36
37
function ChatComponent() {
38
const { messages, sendMessage, status, error, stop } = useChat({
39
onFinish: ({ message }) => console.log('Response finished:', message),
40
onError: (error) => console.error('Error:', error),
41
});
42
43
return (
44
<div>
45
{messages.map((message) => (
46
<div key={message.id}>
47
<strong>{message.role}:</strong>
48
{message.parts.map((part) =>
49
part.type === 'text' && <span key={part.text}>{part.text}</span>
50
)}
51
</div>
52
))}
53
54
<button onClick={() => sendMessage({ text: 'Hello!' })} disabled={status === 'streaming'}>
55
Send
56
</button>
57
58
{status === 'streaming' && <button onClick={stop}>Stop</button>}
59
{error && <div>{error.message}</div>}
60
</div>
61
);
62
}
63
```
64
65
## Production Patterns
66
67
### Error Handling with Retry
68
69
```typescript
70
import { useChat } from '@ai-sdk/react';
71
import { useState } from 'react';
72
73
function ResilientChat() {
74
const [retryCount, setRetryCount] = useState(0);
75
76
const { messages, sendMessage, error, clearError, status } = useChat({
77
onError: (error) => {
78
console.error('Chat error:', error);
79
// Auto-retry on network errors
80
if (error.message.includes('network') && retryCount < 3) {
81
setTimeout(() => {
82
setRetryCount(prev => prev + 1);
83
clearError();
84
// Resend last message
85
}, 1000 * Math.pow(2, retryCount));
86
}
87
},
88
onFinish: () => setRetryCount(0), // Reset on success
89
});
90
91
const handleSend = async (text: string) => {
92
try {
93
await sendMessage({ text });
94
} catch (err) {
95
// Handle in onError callback
96
}
97
};
98
99
return (
100
<div>
101
{error && (
102
<div className="error-banner">
103
<p>Error: {error.message}</p>
104
{retryCount > 0 && <p>Retry attempt {retryCount}/3...</p>}
105
<button onClick={() => { clearError(); handleSend('Retry') }}>
106
Retry Manually
107
</button>
108
</div>
109
)}
110
111
{messages.map(m => <div key={m.id}>{/* Render message */}</div>)}
112
113
<button onClick={() => handleSend('Hello')} disabled={status === 'streaming'}>
114
Send
115
</button>
116
</div>
117
);
118
}
119
```
120
121
### File Upload Support
122
123
```typescript
124
import { useChat } from '@ai-sdk/react';
125
import { useRef } from 'react';
126
127
function ChatWithFiles() {
128
const { messages, sendMessage, status } = useChat();
129
const fileInputRef = useRef<HTMLInputElement>(null);
130
131
const handleSendWithFiles = async () => {
132
const files = fileInputRef.current?.files;
133
const text = 'Analyze these images';
134
135
if (files && files.length > 0) {
136
// Send message with files
137
await sendMessage({ text, files });
138
// Clear file input
139
if (fileInputRef.current) fileInputRef.current.value = '';
140
} else {
141
await sendMessage({ text });
142
}
143
};
144
145
return (
146
<div>
147
{messages.map((message) => (
148
<div key={message.id}>
149
{message.parts.map((part, i) => {
150
if (part.type === 'text') {
151
return <p key={i}>{part.text}</p>;
152
}
153
if (part.type === 'file' && part.mediaType.startsWith('image/')) {
154
return <img key={i} src={part.url} alt={part.filename} />;
155
}
156
return null;
157
})}
158
</div>
159
))}
160
161
<input
162
ref={fileInputRef}
163
type="file"
164
multiple
165
accept="image/*"
166
disabled={status === 'streaming'}
167
/>
168
<button onClick={handleSendWithFiles} disabled={status === 'streaming'}>
169
Send with Files
170
</button>
171
</div>
172
);
173
}
174
```
175
176
### Tool Call Handling
177
178
```typescript
179
import { useChat } from '@ai-sdk/react';
180
181
// Define your tools with proper types
182
type MyTools = {
183
getWeather: {
184
input: { location: string };
185
output: { temp: number; conditions: string };
186
};
187
searchWeb: {
188
input: { query: string };
189
output: { results: string[] };
190
};
191
};
192
193
interface MyMessage extends UIMessage<unknown, UIDataTypes, MyTools> {}
194
195
function ChatWithTools() {
196
const { messages, sendMessage, addToolResult } = useChat<MyMessage>({
197
// IMPORTANT: onToolCall returns void, not the result
198
// Use it for logging or triggering side effects
199
onToolCall: ({ toolCall }) => {
200
console.log('Tool called:', toolCall.toolName, toolCall.args);
201
},
202
203
// Auto-retry tool calls that fail
204
onFinish: async ({ message }) => {
205
const failedTools = message.parts.filter(
206
p => (p.type.startsWith('tool-') || p.type === 'dynamic-tool') && p.state === 'output-error'
207
);
208
209
if (failedTools.length > 0) {
210
console.log('Some tools failed, consider retry logic');
211
}
212
},
213
});
214
215
// Execute tool calls manually with proper error handling
216
const executeToolCall = async (toolName: keyof MyTools, toolCallId: string, args: any) => {
217
try {
218
let output;
219
220
if (toolName === 'getWeather') {
221
const response = await fetch(`/api/weather?location=${args.location}`);
222
if (!response.ok) throw new Error('Weather API failed');
223
output = await response.json();
224
} else if (toolName === 'searchWeb') {
225
const response = await fetch('/api/search', {
226
method: 'POST',
227
body: JSON.stringify({ query: args.query }),
228
});
229
if (!response.ok) throw new Error('Search API failed');
230
output = await response.json();
231
}
232
233
// Add successful result
234
await addToolResult({
235
tool: toolName,
236
toolCallId,
237
output,
238
});
239
} catch (error) {
240
// Add error result
241
await addToolResult({
242
tool: toolName,
243
toolCallId,
244
state: 'output-error',
245
errorText: error instanceof Error ? error.message : 'Tool execution failed',
246
});
247
}
248
};
249
250
// Render tool calls in UI
251
const renderToolCall = (part: ToolUIPart<MyTools>) => {
252
const toolName = part.type.slice(5); // Remove 'tool-' prefix
253
254
return (
255
<div className="tool-call">
256
<strong>🔧 {toolName}</strong>
257
{part.state === 'input-available' && (
258
<>
259
<pre>Input: {JSON.stringify(part.input, null, 2)}</pre>
260
<button onClick={() => executeToolCall(toolName as keyof MyTools, part.toolCallId, part.input)}>
261
Execute
262
</button>
263
</>
264
)}
265
{part.state === 'output-available' && (
266
<pre>Output: {JSON.stringify(part.output, null, 2)}</pre>
267
)}
268
{part.state === 'output-error' && (
269
<div className="error">
270
Error: {part.errorText}
271
<button onClick={() => executeToolCall(toolName as keyof MyTools, part.toolCallId, part.input)}>
272
Retry
273
</button>
274
</div>
275
)}
276
</div>
277
);
278
};
279
280
return (
281
<div>
282
{messages.map((message) => (
283
<div key={message.id}>
284
{message.parts.map((part, i) => {
285
if (part.type === 'text') return <p key={i}>{part.text}</p>;
286
if (part.type.startsWith('tool-')) return <div key={i}>{renderToolCall(part)}</div>;
287
return null;
288
})}
289
</div>
290
))}
291
292
<button onClick={() => sendMessage({ text: 'What is the weather in London?' })}>
293
Ask about weather
294
</button>
295
</div>
296
);
297
}
298
```
299
300
### Automatic Tool Execution
301
302
```typescript
303
function AutoToolChat() {
304
const { messages, sendMessage, addToolResult } = useChat({
305
// Automatically execute tool calls
306
onToolCall: async ({ toolCall }) => {
307
console.log('Auto-executing tool:', toolCall.toolName);
308
309
try {
310
// Execute the tool
311
const result = await executeToolFunction(toolCall.toolName, toolCall.args);
312
313
// Add the result
314
await addToolResult({
315
tool: toolCall.toolName,
316
toolCallId: toolCall.toolCallId,
317
output: result,
318
});
319
} catch (error) {
320
// Add error result
321
await addToolResult({
322
tool: toolCall.toolName,
323
toolCallId: toolCall.toolCallId,
324
state: 'output-error',
325
errorText: error.message,
326
});
327
}
328
},
329
330
// Auto-continue conversation after tool results
331
sendAutomaticallyWhen: ({ messages }) => {
332
const lastMessage = messages[messages.length - 1];
333
return lastMessage?.parts.some(
334
part => (part.type.startsWith('tool-') || part.type === 'dynamic-tool') &&
335
part.state === 'output-available'
336
) || false;
337
},
338
});
339
340
async function executeToolFunction(toolName: string, args: any) {
341
// Your tool execution logic
342
if (toolName === 'getWeather') {
343
const response = await fetch(`/api/weather?location=${args.location}`);
344
return response.json();
345
}
346
throw new Error(`Unknown tool: ${toolName}`);
347
}
348
349
return (
350
<div>
351
{messages.map(m => <div key={m.id}>{/* Render */}</div>)}
352
<button onClick={() => sendMessage({ text: 'What is the weather?' })}>
353
Send
354
</button>
355
</div>
356
);
357
}
358
```
359
360
### Message Editing
361
362
```typescript
363
function EditableChat() {
364
const { messages, setMessages, regenerate, status } = useChat();
365
const [editingId, setEditingId] = useState<string | null>(null);
366
const [editText, setEditText] = useState('');
367
368
const startEdit = (messageId: string, currentText: string) => {
369
setEditingId(messageId);
370
setEditText(currentText);
371
};
372
373
const saveEdit = () => {
374
if (!editingId) return;
375
376
setMessages(messages.map(msg =>
377
msg.id === editingId
378
? { ...msg, parts: [{ type: 'text', text: editText }] }
379
: msg
380
));
381
setEditingId(null);
382
setEditText('');
383
};
384
385
const regenerateFromEdit = async (messageId: string) => {
386
await regenerate({ messageId });
387
};
388
389
return (
390
<div>
391
{messages.map((message) => {
392
const textPart = message.parts.find(p => p.type === 'text');
393
const isEditing = editingId === message.id;
394
395
return (
396
<div key={message.id}>
397
{isEditing ? (
398
<>
399
<textarea value={editText} onChange={e => setEditText(e.target.value)} />
400
<button onClick={saveEdit}>Save</button>
401
<button onClick={() => setEditingId(null)}>Cancel</button>
402
</>
403
) : (
404
<>
405
<p>{textPart?.text}</p>
406
{message.role === 'user' && (
407
<button onClick={() => startEdit(message.id, textPart?.text || '')}>
408
Edit
409
</button>
410
)}
411
{message.role === 'assistant' && (
412
<button onClick={() => regenerateFromEdit(message.id)} disabled={status === 'streaming'}>
413
Regenerate
414
</button>
415
)}
416
</>
417
)}
418
</div>
419
);
420
})}
421
</div>
422
);
423
}
424
```
425
426
### Streaming State Management
427
428
```typescript
429
function StreamingChat() {
430
const { messages, sendMessage, status, stop } = useChat({
431
experimental_throttle: 100, // Update every 100ms for smoother rendering
432
});
433
434
// Track streaming state for last message
435
const lastMessage = messages[messages.length - 1];
436
const isStreaming = status === 'streaming';
437
const streamingText = lastMessage?.parts.find(
438
p => p.type === 'text' && p.state === 'streaming'
439
);
440
441
return (
442
<div>
443
{messages.map((message) => (
444
<div key={message.id} className={message.role}>
445
{message.parts.map((part, i) => {
446
if (part.type === 'text') {
447
return (
448
<p key={i}>
449
{part.text}
450
{part.state === 'streaming' && <span className="cursor">â–Š</span>}
451
</p>
452
);
453
}
454
return null;
455
})}
456
</div>
457
))}
458
459
{isStreaming && (
460
<div className="streaming-indicator">
461
<span>AI is typing...</span>
462
<button onClick={stop}>Stop</button>
463
</div>
464
)}
465
466
<button onClick={() => sendMessage({ text: 'Hello' })} disabled={isStreaming}>
467
Send Message
468
</button>
469
</div>
470
);
471
}
472
```
473
474
### Advanced Configuration
475
476
```typescript
477
import { useChat } from '@ai-sdk/react';
478
479
function AdvancedChat() {
480
const chat = useChat({
481
id: 'my-chat',
482
experimental_throttle: 100,
483
resume: true, // Auto-resume interrupted streams on mount
484
485
messages: [
486
{
487
id: 'welcome',
488
role: 'system',
489
parts: [{ type: 'text', text: 'You are a helpful assistant.' }],
490
},
491
],
492
493
onToolCall: ({ toolCall }) => {
494
console.log('Tool called:', toolCall);
495
},
496
497
onFinish: ({ message, isAbort, isDisconnect, isError }) => {
498
if (isError) console.error('Response error');
499
else if (isAbort) console.log('Response aborted');
500
else if (isDisconnect) console.log('Stream disconnected');
501
else console.log('Response complete:', message);
502
},
503
504
onError: (error) => {
505
console.error('Chat error:', error);
506
// Send to error tracking
507
},
508
});
509
510
return (
511
<div>
512
{chat.messages.map(m => <div key={m.id}>{/* Render */}</div>)}
513
<button onClick={() => chat.sendMessage({ text: 'Hello' })}>Send</button>
514
</div>
515
);
516
}
517
```
518
519
## Message Part Types
520
521
```typescript
522
// Text content
523
interface TextUIPart {
524
type: 'text';
525
text: string;
526
state?: 'streaming' | 'done';
527
}
528
529
// AI reasoning (e.g., from models with extended thinking)
530
interface ReasoningUIPart {
531
type: 'reasoning';
532
text: string;
533
state?: 'streaming' | 'done';
534
}
535
536
// File attachments
537
interface FileUIPart {
538
type: 'file';
539
mediaType: string; // MIME type
540
filename?: string;
541
url: string;
542
}
543
544
// External sources
545
interface SourceUrlUIPart {
546
type: 'source-url';
547
sourceId: string;
548
url: string;
549
title?: string;
550
}
551
552
// Tool calls
553
interface ToolUIPart<TOOLS extends UITools = UITools> {
554
type: `tool-${string}`;
555
toolCallId: string;
556
state: 'input-streaming' | 'input-available' | 'output-available' | 'output-error';
557
input?: unknown;
558
output?: unknown;
559
errorText?: string;
560
providerExecuted?: boolean;
561
}
562
```
563
564
## Best Practices
565
566
1. **Type your messages**: Define custom `UIMessage` types with specific metadata, data, and tool types
567
2. **Handle all states**: Show appropriate UI for 'submitted', 'streaming', 'ready', and 'error' states
568
3. **Implement retry logic**: Add exponential backoff for failed requests
569
4. **Use error boundaries**: Wrap chat components in error boundaries
570
5. **Throttle updates**: Use `experimental_throttle` for large responses
571
6. **Tool call errors**: Always handle tool execution failures gracefully
572
7. **Optimize rendering**: Memoize message components to prevent unnecessary re-renders
573
8. **File validation**: Validate file types and sizes before sending
574
9. **Clear user feedback**: Show loading states, errors, and streaming indicators
575
10. **Test edge cases**: Test network failures, tool errors, and stream interruptions
576