or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mduse-chat.mduse-completion.mduse-object.md

use-chat.mddocs/

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