Xây dựng Streaming AI Chat UI trong React — Từ useEffect đến Vercel AI SDK
Hướng dẫn thực tế xây dựng chat interface với streaming responses trong React: từ fetch API với ReadableStream, đến useChat hook của Vercel AI SDK. Bao gồm TypeScript types, loading states, và error handling đúng cách.
Streaming responses — tại sao không phải cứ fetch rồi render?
Khi tích hợp AI vào frontend app, điều đầu tiên mọi người thử là gọi API, chờ full response rồi render. Vấn đề: với LLM, "chờ full response" có thể là 5–15 giây, và user ngồi nhìn spinner. Streaming giải quyết điều này bằng cách render từng token ngay khi AI trả về — UX tốt hơn hoàn toàn.
Bài này đi từ implementation thủ công bằng ReadableStream đến cách dùng Vercel AI SDK để tiết kiệm thời gian.
Cách 1: Tự xử lý với fetch + ReadableStream
Trước khi dùng bất kỳ SDK nào, tôi luôn recommend hiểu cơ chế ở dưới. Đây là cách streaming thực sự hoạt động:
// hooks/useStreamingChat.ts
import { useState, useCallback } from "react";
interface Message {
role: "user" | "assistant";
content: string;
}
export function useStreamingChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const sendMessage = useCallback(async (userInput: string) => {
const userMessage: Message = { role: "user", content: userInput };
setMessages(prev => [...prev, userMessage]);
setIsStreaming(true);
// Thêm placeholder cho assistant response
setMessages(prev => [...prev, { role: "assistant", content: "" }]);
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [...messages, userMessage],
}),
});
if (!response.body) throw new Error("No response body");
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// Parse SSE format: "data: {token}\n\n"
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const token = line.slice(6);
if (token === "[DONE]") continue;
// Append token vào message cuối
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = {
...updated[updated.length - 1],
content: updated[updated.length - 1].content + token,
};
return updated;
});
}
}
}
} catch (error) {
console.error("Streaming error:", error);
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = {
...updated[updated.length - 1],
content: "Có lỗi xảy ra. Vui lòng thử lại.",
};
return updated;
});
} finally {
setIsStreaming(false);
}
}, [messages]);
return { messages, isStreaming, sendMessage };
}
API route tương ứng (Next.js App Router):
// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
controller.enqueue(
encoder.encode(`data: ${event.delta.text}\n\n`)
);
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
Cách 2: Vercel AI SDK — Ít code hơn, nhiều tính năng hơn
Sau khi hiểu cơ chế, dùng Vercel AI SDK sẽ tiết kiệm rất nhiều thời gian:
# Install
npm install ai @ai-sdk/anthropic
// app/api/chat/route.ts — với Vercel AI SDK
import { anthropic } from "@ai-sdk/anthropic";
import { streamText } from "ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-4-6"),
messages,
system: "You are a helpful frontend development assistant.",
});
return result.toDataStreamResponse();
}
// components/Chat.tsx — useChat hook xử lý tất cả
import { useChat } from "ai/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({ api: "/api/chat" });
return (
<div className="flex flex-col h-screen">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(m => (
<div
key={m.id}
className={`flex ${m.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
m.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
{m.content}
{isLoading && m.role === "assistant" && (
<span className="inline-block w-1 h-4 ml-1 bg-current animate-pulse" />
)}
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Nhập câu hỏi..."
className="flex-1 rounded-full border px-4 py-2 outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={isLoading}
className="rounded-full bg-blue-600 px-6 py-2 text-white disabled:opacity-50"
>
Gửi
</button>
</div>
</form>
</div>
);
}
Những gotchas cần chú ý
- Hydration mismatch: Render chat messages phía client — dùng
use clientdirective và tránh SSR cho chat component - Auto-scroll: Dùng
useEffectvớiscrollIntoViewmỗi khimessagesthay đổi, nhưng chỉ scroll nếu user đang ở cuối trang (tránh interrupt khi user đang đọc) - Abort controller: Vercel AI SDK tự handle cancel khi component unmount. Nếu tự implement, nhớ cleanup với
AbortController - Rate limiting: Implement debounce hoặc disable submit button khi đang stream
Kết luận
Streaming AI responses không phức tạp như nhiều người nghĩ — về bản chất chỉ là Server-Sent Events quen thuộc. Hiểu cơ chế thủ công trước, sau đó dùng Vercel AI SDK để tăng tốc development. useChat hook đặc biệt tốt cho prototype nhanh — bạn có thể build chat interface đầy đủ tính năng trong vài giờ thay vì vài ngày.
Với AI ngày càng được tích hợp vào mọi product, biết cách build streaming UI là kỹ năng frontend không thể thiếu trong 2026.
Admin
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!