# AI SDK UI - Streaming Best Practices
UI patterns and best practices for streaming AI responses.
**Last Updated**: 2025-10-22
---
## Performance
### Always Use Streaming for Long-Form Content
```tsx
// ✅ GOOD: Streaming provides better perceived performance
const { messages } = useChat({ api: '/api/chat' });
// ❌ BAD: Blocking - user waits for entire response
const response = await fetch('/api/chat', { method: 'POST' });
```
**Why?**
- Users see tokens as they arrive
- Perceived performance is much faster
- Users can start reading before response completes
- Can stop generation early
---
## UX Patterns
### 1. Show Loading States
```tsx
const { messages, isLoading } = useChat();
{isLoading && (
)}
```
### 2. Provide Stop Button
```tsx
const { isLoading, stop } = useChat();
{isLoading && (
)}
```
### 3. Auto-Scroll to Latest Message
```tsx
const messagesEndRef = useRef(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
```
### 4. Disable Input While Loading
```tsx
setInput(e.target.value)}
disabled={isLoading} // Prevent new messages while generating
className="disabled:bg-gray-100"
/>
```
### 5. Handle Empty States
```tsx
{messages.length === 0 ? (
Start a conversation
Ask me anything!
) : (
// Messages list
)}
```
---
## Error Handling
### 1. Display Errors to Users
```tsx
const { error } = useChat();
{error && (
Error: {error.message}
)}
```
### 2. Provide Retry Functionality
```tsx
const { error, reload } = useChat();
{error && (
{error.message}
)}
```
### 3. Handle Network Failures Gracefully
```tsx
useChat({
onError: (error) => {
console.error('Chat error:', error);
// Log to monitoring service (Sentry, etc.)
// Show user-friendly message
},
});
```
### 4. Log Errors for Debugging
```tsx
useChat({
onError: (error) => {
const errorLog = {
timestamp: new Date().toISOString(),
message: error.message,
url: window.location.href,
};
console.error('AI SDK Error:', errorLog);
// Send to Sentry/Datadog/etc.
},
});
```
---
## Message Rendering
### 1. Support Markdown
Use `react-markdown` for rich content:
```tsx
import ReactMarkdown from 'react-markdown';
{messages.map(m => (
{m.content}
))}
```
### 2. Handle Code Blocks
```tsx
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
{String(children)}
) : (
{children}
);
},
}}
>
{message.content}
```
### 3. Display Tool Calls Visually
```tsx
{message.toolInvocations?.map((tool, idx) => (
Tool: {tool.toolName}
Args: {JSON.stringify(tool.args)}
{tool.result && (
Result: {JSON.stringify(tool.result)}
)}
))}
```
### 4. Show Timestamps
```tsx
{new Date(message.createdAt).toLocaleTimeString()}
```
### 5. Group Messages by Role
```tsx
{messages.reduce((groups, message, idx) => {
const prevMessage = messages[idx - 1];
const showRole = !prevMessage || prevMessage.role !== message.role;
return [
...groups,
{showRole &&
{message.role}
}
{message.content}
];
}, [])}
```
---
## State Management
### 1. Persist Chat History
```tsx
const chatId = 'chat-123';
const { messages } = useChat({
id: chatId,
initialMessages: loadFromLocalStorage(chatId),
});
useEffect(() => {
saveToLocalStorage(chatId, messages);
}, [messages, chatId]);
```
### 2. Clear Chat Functionality
```tsx
const { setMessages } = useChat();
const clearChat = () => {
if (confirm('Clear chat history?')) {
setMessages([]);
}
};
```
### 3. Export/Import Conversations
```tsx
const exportChat = () => {
const json = JSON.stringify(messages, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat-${Date.now()}.json`;
a.click();
};
const importChat = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const imported = JSON.parse(e.target?.result as string);
setMessages(imported);
};
reader.readAsText(file);
};
```
### 4. Handle Multiple Chats (Routing)
```tsx
// Use URL params for chat ID
const searchParams = useSearchParams();
const chatId = searchParams.get('chatId') || 'default';
const { messages } = useChat({
id: chatId,
initialMessages: loadMessages(chatId),
});
// Navigation
New Chat
```
---
## Advanced Patterns
### 1. Debounced Input for Completions
```tsx
import { useDebouncedCallback } from 'use-debounce';
const { complete } = useCompletion();
const debouncedComplete = useDebouncedCallback((value) => {
complete(value);
}, 500);
debouncedComplete(e.target.value)} />
```
### 2. Optimistic Updates
```tsx
const { messages, sendMessage } = useChat();
const optimisticSend = (content: string) => {
// Add user message immediately
const tempMessage = {
id: `temp-${Date.now()}`,
role: 'user',
content,
};
setMessages([...messages, tempMessage]);
// Send to server
sendMessage({ content });
};
```
### 3. Custom Message Formatting
```tsx
const formatMessage = (content: string) => {
// Replace @mentions
content = content.replace(/@(\w+)/g, '@$1');
// Replace URLs
content = content.replace(
/(https?:\/\/[^\s]+)/g,
'$1'
);
return content;
};
```
### 4. Typing Indicators
```tsx
const [isTyping, setIsTyping] = useState(false);
useChat({
onFinish: () => setIsTyping(false),
});
const handleSend = (content: string) => {
setIsTyping(true);
sendMessage({ content });
};
{isTyping && AI is typing...
}
```
---
## Performance Optimization
### 1. Virtualize Long Message Lists
```tsx
import { FixedSizeList } from 'react-window';
{({ index, style }) => (
{messages[index].content}
)}
```
### 2. Lazy Load Message History
```tsx
const [page, setPage] = useState(1);
const messagesPerPage = 50;
const visibleMessages = messages.slice(
(page - 1) * messagesPerPage,
page * messagesPerPage
);
```
### 3. Memoize Message Rendering
```tsx
import { memo } from 'react';
const MessageComponent = memo(({ message }: { message: Message }) => {
return {message.content}
;
});
{messages.map(m => )}
```
---
## Official Documentation
- **AI SDK UI Overview**: https://ai-sdk.dev/docs/ai-sdk-ui/overview
- **Streaming Protocols**: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocols
- **Message Metadata**: https://ai-sdk.dev/docs/ai-sdk-ui/message-metadata
---
**Last Updated**: 2025-10-22