← Blog

Claude API Streaming Response React Example

2026-10-04 · 4 min read · SubToAPI Team

Streaming Claude's responses in a React app means rendering tokens as they arrive instead of waiting for the full completion. This guide shows a complete, working pattern: a fetch call with a readable stream, a parser for server-sent events (SSE), and React state updates that append text as chunks come in.

The short answer: you don't need a special SDK to stream Claude responses in React. You need fetch with response.body.getReader(), a loop that decodes chunks, a parser that splits on SSE event boundaries, and a useState call that appends text on each chunk. Below is the full implementation, plus the backend route it talks to.

Why Streaming Matters for Claude in React

Without streaming, a user staring at a blank screen for 3–8 seconds while Claude generates a long answer feels broken. Streaming fixes the perceived latency problem — text starts appearing within a few hundred milliseconds, even if the full response takes just as long to finish generating.

The tradeoff is complexity: you're now parsing a stream of SSE events instead of reading one JSON blob. That's the part most tutorials gloss over.

The Backend Route

Browsers can't safely hold an API key, so you need a backend route that calls Claude (or a Claude-compatible endpoint) and pipes the stream through. Here's a minimal Node/Express route using a Claude-style streaming API:

// server.js
app.post('/api/chat-stream', async (req, res) => {
  const { messages } = req.body;

  const upstream = await fetch('https://api.subtoapi.app/v1/messages', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'claude-sonnet-4-5',
      max_tokens: 1024,
      stream: true,
      messages
    })
  });

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const reader = upstream.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    res.write(decoder.decode(value));
  }
  res.end();
});

This route just relays the upstream SSE stream to the client unchanged. Using SubToAPI here means you authenticate with a single sub_live_... key instead of juggling raw Anthropic credentials, and you get usage metadata on every call for free — useful if you're billing teammates per-request later.

The React Hook

Now the client side. This hook manages the fetch, reads the stream, parses SSE chunks, and updates state as text arrives:

// useStreamingChat.js
import { useState, useCallback } from 'react';

export function useStreamingChat() {
  const [text, setText] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);

  const sendMessage = useCallback(async (messages) => {
    setText('');
    setIsStreaming(true);

    const res = await fetch('/api/chat-stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ messages })
    });

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop(); // keep incomplete line for next chunk

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;
        const payload = line.slice(6).trim();
        if (payload === '[DONE]') continue;

        try {
          const event = JSON.parse(payload);
          if (event.type === 'content_block_delta') {
            setText((prev) => prev + event.delta.text);
          }
        } catch {
          // ignore partial/malformed chunks, handled by buffer
        }
      }
    }

    setIsStreaming(false);
  }, []);

  return { text, isStreaming, sendMessage };
}

The key detail: buffer incomplete lines. SSE chunks don't arrive aligned to line boundaries, so splitting on \n and immediately parsing every line will throw on cut-off JSON. Keeping the last (possibly incomplete) line in buffer for the next iteration avoids that.

The Component

// Chat.jsx
import { useStreamingChat } from './useStreamingChat';
import { useState } from 'react';

export default function Chat() {
  const [input, setInput] = useState('');
  const { text, isStreaming, sendMessage } = useStreamingChat();

  const handleSubmit = (e) => {
    e.preventDefault();
    sendMessage([{ role: 'user', content: input }]);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={(e) => setInput(e.target.value)} />
        <button disabled={isStreaming}>Send</button>
      </form>
      <p>{text}{isStreaming && <span className="cursor">▋</span>}</p>
    </div>
  );
}

That's a full round trip: user input → backend relay → SSE parsing → incremental React state → rendered text with a blinking cursor while streaming.

Common Pitfalls

If you'd rather skip building and maintaining the backend relay yourself, SubToAPI exposes a standard /v1/messages endpoint with stream: true support, so the server-side piece above is close to drop-in. Check the streaming docs for event-type specifics and the quickstart for key setup.

Questions

Does Claude support true token-by-token streaming, or just chunked responses? It streams incrementally via SSE events (content_block_delta carries text pieces), which is functionally token-by-token for UI purposes, though exact chunk size can vary.

Can I stream directly from the browser without a backend? Not safely — it would expose your API key in client-side code. Always proxy through a backend route, as shown above.

Why does my React UI lag or flicker during streaming? Usually it's a state update on every single chunk causing excessive re-renders. Batch updates or throttle setText calls to smooth it out.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →