Bỏ qua để vào nội dung chính
Streaming AI Responses in React: Techniques and Patterns

Streaming AI Responses in React: Techniques and Patterns

Bởi Hannah Cooper
17 thg 3, 20266 phút đọc

Streaming makes AI responses feel instant by showing text as it generates. Learn the three patterns for streaming AI in React — from simple to advanced — and when to use each.

Why Streaming Transforms AI UX

Without streaming, users wait 3-8 seconds staring at a loading spinner before seeing any response. With streaming, text appears within 200-400ms and flows in naturally — just like watching someone type. This single change makes AI interfaces feel dramatically more responsive.

Pattern 1: Using the useChat Hook (Simplest)

The Vercel AI SDK's useChat handles streaming automatically. This is the right choice for 90% of chat interfaces:

'use client';
import { useChat } from 'ai/react';

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

  return (
    <div>
      {messages.map(m => (
        <div key={m.id} className={m.role}>>
          {/* Text appears incrementally as it streams */}
          <p>{m.content}</p>
        </div>
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
        <button type="submit" disabled={isLoading}>Send</button>
      </form>
    </div>
  );
}

Pattern 2: Manual Fetch With ReadableStream (More Control)

When you need custom handling or aren't using the AI SDK's hooks:

'use client';
import { useState } from 'react';

export function ManualStreamingComponent() {
  const [response, setResponse] = useState('');
  const [loading, setLoading] = useState(false);

  async function sendMessage(prompt: string) {
    setLoading(true);
    setResponse('');
    
    const res = await fetch('/api/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt }),
    });

    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const text = decoder.decode(value);
      setResponse(prev => prev + text);
    }
    setLoading(false);
  }

  return (
    <div>
      <p className="whitespace-pre-wrap">{response}</p>
      {loading && <span className="animate-pulse">▋</span>}
    </div>
  );
}

Pattern 3: Server-Sent Events for Real-Time Updates

For cases where the server needs to push multiple streaming updates (like agent progress):

// app/api/agent-run/route.ts
export async function POST(request: Request) {
  const encoder = new TextEncoder();
  
  const stream = new ReadableStream({
    async start(controller) {
      function send(data: object) {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
      }

      send({ type: 'status', message: 'Starting analysis...' });
      await runStep1();
      send({ type: 'status', message: 'Processing results...' });
      await runStep2();
      send({ type: 'complete', result: 'Done!' });
      controller.close();
    }
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    }
  });
}
// Client: consume SSE with EventSource
const es = new EventSource('/api/agent-run');
es.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === 'status') setStatus(data.message);
  if (data.type === 'complete') {
    setResult(data.result);
    es.close();
  }
};

Rendering Markdown From Streaming Text

Streaming markdown needs special handling to avoid hydration errors as partial syntax arrives:

import ReactMarkdown from 'react-markdown';

function StreamingMarkdown({ content }: { content: string }) {
  // Add a cursor to indicate streaming is active
  const displayContent = content + '▋';
  
  return (
    <div className="prose">
      <ReactMarkdown>{displayContent}</ReactMarkdown>
    </div>
  );
}

Handling Network Interruptions

Always handle stream failures gracefully with retry logic and clear error states. The AI SDK's useChat handles this automatically; for manual streams, wrap in try/catch and expose a retry button. Users accept occasional failures far better than they accept hangs with no feedback.

Không spam, hủy đăng ký bất kỳ lúc nào.

Bài viết liên quan