← Blog

Streaming Claude API Responses in a React Frontend

2026-11-08 · 5 min read · SubToAPI Team

Streaming a Claude API response into a React frontend means rendering tokens as they arrive instead of waiting for the full completion. The pattern is: your backend (or an API proxy) opens a streaming connection to Claude, forwards each chunk as a server-sent event, and your React component reads that stream with fetch + ReadableStream, appending text to state as it comes in.

This is the part most developers get stuck on — not the Claude API call itself, but correctly consuming a stream in the browser without a library, handling partial chunks, and updating React state efficiently without re-rendering on every single byte. Below is a complete, working approach you can adapt directly.

Why stream at all

Claude responses for anything non-trivial (long explanations, code generation, structured reports) can take several seconds to several tens of seconds to complete. Without streaming, your UI sits on a blank loading spinner the whole time. With streaming:

Claude's native streaming API emits server-sent events with content_block_delta chunks containing incremental text. The React job is just: consume those events and append the text pieces to a string in state.

Setting up the stream endpoint

You generally don't call Claude directly from the browser — you need a server endpoint that holds the actual API key and streams the response through. If you're using SubToAPI, this is already handled: you get an sub_live_... key scoped to your app, and the streaming endpoint behaves like a standard SSE response, so your backend proxy code is a thin pass-through. See /docs/streaming for the exact event shapes.

A minimal Node/Express proxy looks like this:

app.post("/api/chat/stream", async (req, res) => {
  const response = 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",
      max_tokens: 1024,
      stream: true,
      messages: req.body.messages,
    }),
  });

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  response.body.pipe(res);
});

This keeps the API key server-side and just relays the SSE stream to your frontend untouched. Never call Claude, or SubToAPI, directly from client-side JavaScript with a real key — it will leak in the browser.

Consuming the stream in React

On the frontend, use fetch with a ReadableStream reader, decode chunks, split on SSE event boundaries (\n\n), and parse the JSON payload of each event.

import { useState, useRef } from "react";

function useClaudeStream() {
  const [text, setText] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const abortRef = useRef(null);

  async function sendMessage(messages) {
    setText("");
    setIsStreaming(true);
    const controller = new AbortController();
    abortRef.current = controller;

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

    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 events = buffer.split("\n\n");
      buffer = events.pop(); // keep incomplete event for next loop

      for (const rawEvent of events) {
        const line = rawEvent.replace(/^data:\s*/, "").trim();
        if (!line || line === "[DONE]") continue;

        try {
          const parsed = JSON.parse(line);
          if (parsed.type === "content_block_delta") {
            setText((prev) => prev + parsed.delta.text);
          }
        } catch {
          // ignore keep-alive or malformed partial lines
        }
      }
    }

    setIsStreaming(false);
  }

  function stop() {
    abortRef.current?.abort();
    setIsStreaming(false);
  }

  return { text, isStreaming, sendMessage, stop };
}

A component then just consumes the hook:

function Chat() {
  const { text, isStreaming, sendMessage, stop } = useClaudeStream();

  return (
    <div>
      <button onClick={() => sendMessage([{ role: "user", content: "Explain closures in JS" }])}>
        Ask
      </button>
      {isStreaming && <button onClick={stop}>Stop</button>}
      <p>{text}</p>
    </div>
  );
}

Common issues and how to avoid them

Chunks arrive mid-JSON. SSE frames aren't guaranteed to align with your parse boundaries, especially behind proxies or with HTTP/2. Always buffer and split on \n\n rather than assuming one read() call equals one event.

Re-renders feel laggy on long responses. Appending to a string with setState on every token can cause excessive renders for very long completions. Batch deltas with a small debounce (e.g., flush every 30–50ms) if you notice jank on low-end devices.

CORS and key exposure. If you try to stream directly from the browser to an LLM API, you'll hit CORS issues and expose your key. Always proxy through your own backend route, even if it's a one-liner.

Stopping a stream doesn't stop billing instantly. Calling AbortController.abort() closes the connection client-side, but check your provider's docs on how generation is billed for partial streams — usage is typically metered for tokens actually generated up to the point of cancellation.

If you're evaluating providers for this, SubToAPI exposes the same streaming contract shown above with per-request usage metadata, so you can track token consumption per user or per feature without building your own metering layer. Plans start with a free trial at /signup, and pricing details are at /pricing.

Testing your streaming setup

Before wiring up React, confirm the raw stream works with curl:

curl -N https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4",
    "max_tokens": 256,
    "stream": true,
    "messages": [{"role": "user", "content": "Count to 10 slowly"}]
  }'

The -N flag disables curl's output buffering so you see events as they arrive, which is the quickest way to confirm the server side is actually streaming before you debug the frontend. Full request/response shapes are documented at /docs/messages and /docs/quickstart.

questions

Do I need a library to stream Claude responses in React? No. fetch with ReadableStream and TextDecoder is sufficient. Libraries like eventsource-parser can simplify SSE parsing but aren't required for a basic implementation.

Can I stream directly from the browser without a backend? Not safely. You'd expose your API key client-side and likely hit CORS restrictions. Always proxy the stream through a backend route you control.

Why does my React UI freeze or lag during streaming? Usually excessive re-renders from calling setState on every token. Batch text updates with a short debounce interval, or use a ref for raw text and flush to state periodically.

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 →