# useChat v4 → v5 Migration Guide

Complete guide to migrating from AI SDK v4 to v5 for UI hooks.

**Last Updated**: 2025-10-22
**Applies to**: AI SDK v5.0+

---

## Critical Breaking Change

**BREAKING: useChat no longer manages input state!**

In v4, `useChat` provided `input`, `handleInputChange`, and `handleSubmit`. In v5, you must manage input state manually using `useState`.

---

## Quick Migration Checklist

- [ ] Replace `input`, `handleInputChange`, `handleSubmit` with manual state
- [ ] Change `append()` to `sendMessage()`
- [ ] Replace `onResponse` with `onFinish`
- [ ] Move `initialMessages` to controlled mode with `messages` prop
- [ ] Remove `maxSteps` (handle server-side)
- [ ] Update message rendering for parts structure (if using tools)

---

## 1. Input State Management (CRITICAL)

### v4 (OLD)

```tsx
import { useChat } from 'ai/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: '/api/chat',
  });

  return (
    <div>
      {messages.map(m => <div key={m.id}>{m.content}</div>)}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
      </form>
    </div>
  );
}
```

### v5 (NEW)

```tsx
import { useChat } from 'ai/react';
import { useState, FormEvent } from 'react';

export default function Chat() {
  const { messages, sendMessage } = useChat({
    api: '/api/chat',
  });

  // Manual input state
  const [input, setInput] = useState('');

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    sendMessage({ content: input });
    setInput('');
  };

  return (
    <div>
      {messages.map(m => <div key={m.id}>{m.content}</div>)}
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
        />
      </form>
    </div>
  );
}
```

**Why?**
- More control over input handling
- Easier to add features like debouncing, validation, etc.
- Consistent with React patterns

---

## 2. append() → sendMessage()

### v4 (OLD)

```tsx
const { append } = useChat();

// Append a message
append({
  role: 'user',
  content: 'Hello',
});
```

### v5 (NEW)

```tsx
const { sendMessage } = useChat();

// Send a message (role is assumed to be 'user')
sendMessage({
  content: 'Hello',
});

// With attachments
sendMessage({
  content: 'Analyze this image',
  experimental_attachments: [
    { name: 'image.png', contentType: 'image/png', url: 'blob:...' },
  ],
});
```

**Why?**
- Clearer API: `sendMessage` is more intuitive than `append`
- Supports attachments natively
- Role is always 'user' (no need to specify)

---

## 3. onResponse → onFinish

### v4 (OLD)

```tsx
const { messages } = useChat({
  onResponse: (response) => {
    console.log('Response received:', response);
  },
});
```

### v5 (NEW)

```tsx
const { messages } = useChat({
  onFinish: (message, options) => {
    console.log('Response finished:', message);
    console.log('Finish reason:', options.finishReason);
    console.log('Usage:', options.usage);
  },
});
```

**Why?**
- `onResponse` fired too early (when response started)
- `onFinish` fires when response is complete
- Provides more context (usage, finish reason)

---

## 4. initialMessages → Controlled Mode

### v4 (OLD)

```tsx
const { messages } = useChat({
  initialMessages: [
    { role: 'system', content: 'You are a helpful assistant.' },
  ],
});
```

### v5 (NEW - Option 1: Uncontrolled)

```tsx
const { messages } = useChat({
  // Use initialMessages for read-only initialization
  initialMessages: [
    { role: 'system', content: 'You are a helpful assistant.' },
  ],
});
```

### v5 (NEW - Option 2: Controlled)

```tsx
const [messages, setMessages] = useState([
  { role: 'system', content: 'You are a helpful assistant.' },
]);

const { sendMessage } = useChat({
  messages,  // Pass messages for controlled mode
  onUpdate: ({ messages }) => {
    setMessages(messages);  // Sync state
  },
});
```

**Why?**
- Clearer distinction between controlled and uncontrolled
- Easier to persist messages to database

---

## 5. maxSteps Removed

### v4 (OLD)

```tsx
const { messages } = useChat({
  maxSteps: 5,  // Limit agent steps
});
```

### v5 (NEW)

Handle `maxSteps` (or `stopWhen`) on the **server-side** only:

```typescript
// app/api/chat/route.ts
import { streamText, stopWhen } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai('gpt-5'),
    messages,
    maxSteps: 5,  // Handle on server
  });

  return result.toDataStreamResponse();
}
```

**Why?**
- Server has more control over costs
- Prevents client-side bypass
- Consistent with v5 architecture

---

## 6. Message Structure (for Tools)

### v4 (OLD)

```tsx
// Simple message structure
{
  id: '1',
  role: 'assistant',
  content: 'The weather is sunny',
  toolCalls: [...]  // Tool calls as separate property
}
```

### v5 (NEW)

```tsx
// Parts-based structure
{
  id: '1',
  role: 'assistant',
  content: 'The weather is sunny',  // Still exists for simple messages
  parts: [
    { type: 'text', content: 'The weather is' },
    { type: 'tool-call', toolName: 'getWeather', args: { location: 'SF' } },
    { type: 'tool-result', toolName: 'getWeather', result: { temp: 72 } },
    { type: 'text', content: 'sunny' },
  ]
}
```

**Rendering v5 Messages:**

```tsx
messages.map(message => {
  // For simple text messages, use content
  if (message.content) {
    return <div>{message.content}</div>;
  }

  // For tool calls, use toolInvocations
  if (message.toolInvocations) {
    return message.toolInvocations.map(tool => (
      <div key={tool.toolCallId}>
        Tool: {tool.toolName}
        Args: {JSON.stringify(tool.args)}
        Result: {JSON.stringify(tool.result)}
      </div>
    ));
  }
});
```

---

## 7. Other Removed/Changed Properties

### Removed in v5

- `input` - Use manual `useState`
- `handleInputChange` - Use `onChange={(e) => setInput(e.target.value)}`
- `handleSubmit` - Use custom submit handler
- `onResponse` - Use `onFinish` instead

### Renamed in v5

- `append()` → `sendMessage()`
- `initialMessages` → Still exists, but use `messages` prop for controlled mode

### Added in v5

- `sendMessage()` - New way to send messages
- `experimental_attachments` - File attachments support
- `toolInvocations` - Simplified tool call rendering

---

## Common Migration Patterns

### Pattern 1: Basic Chat

**v4:**
```tsx
const { messages, input, handleInputChange, handleSubmit } = useChat();
<form onSubmit={handleSubmit}>
  <input value={input} onChange={handleInputChange} />
</form>
```

**v5:**
```tsx
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>
```

### Pattern 2: With Initial Messages

**v4:**
```tsx
const { messages } = useChat({
  initialMessages: loadFromStorage(),
});
```

**v5:**
```tsx
const { messages } = useChat({
  initialMessages: loadFromStorage(),  // Still works
});
```

### Pattern 3: With Response Callback

**v4:**
```tsx
useChat({
  onResponse: (res) => console.log('Started'),
});
```

**v5:**
```tsx
useChat({
  onFinish: (msg, opts) => {
    console.log('Finished');
    console.log('Tokens:', opts.usage.totalTokens);
  },
});
```

---

## Migration Troubleshooting

### Error: "input is undefined"

**Cause**: You're using v5 but trying to access `input` from `useChat`.

**Fix**: Add manual input state:
```tsx
const [input, setInput] = useState('');
```

### Error: "append is not a function"

**Cause**: `append()` was renamed to `sendMessage()` in v5.

**Fix**: Replace all instances of `append()` with `sendMessage()`.

### Error: "handleSubmit is undefined"

**Cause**: v5 doesn't provide `handleSubmit`.

**Fix**: Create custom submit handler:
```tsx
const handleSubmit = (e: FormEvent) => {
  e.preventDefault();
  sendMessage({ content: input });
  setInput('');
};
```

### Warning: "onResponse is deprecated"

**Cause**: v5 removed `onResponse`.

**Fix**: Use `onFinish` instead.

---

## Official Migration Resources

- **v5 Migration Guide**: https://ai-sdk.dev/docs/migration-guides/migration-guide-5-0
- **useChat API Reference**: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat
- **v5 Release Notes**: https://vercel.com/blog/ai-sdk-5

---

**Last Updated**: 2025-10-22
