← Blog

Claude API Streaming with React Frontend: A Guide

2026-10-09 · 5 min read · SubToAPI Team

Claude API streaming with a React frontend

If you're building a chat-style UI and want Claude's responses to appear word-by-word instead of popping in all at once after a 10-second wait, you need streaming. The short answer: you open a server-sent event (SSE) stream from your backend to Claude, forward those chunks to your React app over fetch with a ReadableStream, and update component state as each token arrives. This guide covers the full setup — backend proxy, frontend consumer, and the gotchas that trip people up.

The reason this requires a backend in between (rather than calling the API directly from the browser) is simple: your Claude or SubToAPI key would be exposed in client-side JavaScript if you called the API from React directly. Every production setup proxies the request through your own server, which also gives you a place to do auth, rate limiting, and logging.

Why streaming matters here

Non-streaming responses block until the full completion is generated. For short answers that's fine. For anything longer — code generation, explanations, multi-paragraph content — users stare at a spinner for several seconds. Streaming sends tokens as they're generated, so the UI feels responsive immediately, even if total generation time is unchanged. This is the single biggest perceived-performance win you can make in an LLM-backed product.

Architecture

React app  →  your backend endpoint  →  Claude API (via SubToAPI)
   ↑                                            |
   └──────────── SSE chunks streamed back ──────┘

Your backend does two jobs: it holds the real API key, and it relays the stream without buffering the whole response first.

Backend: proxying a streaming request

Using SubToAPI's streaming endpoint from a Node/Express server:

// server.js
app.post('/api/chat', 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-opus-4',
      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 just relays bytes through — no buffering, no waiting for the full response. SubToAPI's streaming format follows the same SSE event structure documented at /docs/streaming, so if you've worked with the standard Claude messages API before, nothing here is unfamiliar — see /docs/messages for the base request shape.

Frontend: consuming the stream in React

On the client, use fetch with a ReadableStream reader instead of EventSource, because EventSource only supports GET requests and you need to POST the conversation history.

import { useState } from 'react';

function Chat() {
  const [reply, setReply] = useState('');
  const [loading, setLoading] = useState(false);

  async function sendMessage(text) {
    setReply('');
    setLoading(true);

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

    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\n');
      buffer = lines.pop(); // keep incomplete chunk for next read

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

        try {
          const parsed = JSON.parse(data);
          if (parsed.type === 'content_block_delta') {
            setReply((prev) => prev + parsed.delta.text);
          }
        } catch (_) {
          // ignore partial/non-JSON lines
        }
      }
    }
    setLoading(false);
  }

  return (
    <div>
      <p>{reply}</p>
      {loading && <span>streaming…</span>}
    </div>
  );
}

Key details that matter:

Handling tool use in a streaming context

If your app calls tools mid-conversation, streaming gets slightly more involved: you'll receive content_block_start events for tool calls, incremental input_json_delta events as arguments stream in, and a content_block_stop when the call is complete. Your React state needs to track which content block is active (text vs. tool call) rather than assuming everything is plain text. Details and examples are in /docs/tools.

Common mistakes

If you're prototyping this and don't want to stand up your own Claude account and billing, SubToAPI turns your existing Claude access into an HTTPS API with streaming, tool use, and usage metadata already wired up — plans start at €9/month on Solo, with a free trial at /signup. The quickstart at /docs/quickstart has you streaming your first response in a few minutes.

questions

Does React need any special library to consume SSE streams? No. fetch with response.body.getReader() and TextDecoder is enough. EventSource is simpler but only supports GET requests, which doesn't work for sending conversation history in a POST body.

Why does my stream show garbled text with code blocks? Almost always a UTF-8 decoding issue — use decoder.decode(value, { stream: true }) instead of decoding each chunk independently, since multi-byte characters can split across network reads.

Can I stream directly from React to Claude without a backend? Not safely. Your API key would be exposed in the browser. Always proxy through a server you control, even if that server does nothing but relay bytes.

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 →