← Blog

Claude API React Streaming: Frontend Integration Guide

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

If you're building a chat interface or AI-assisted feature in React, you want tokens to appear as they're generated, not after the full response finishes. That's what streaming gives you: a ReadableStream of server-sent events that you parse incrementally and render into state as each chunk arrives. This article walks through a working integration pattern — a custom hook, a fetch-based reader loop, and the UI wiring needed to make it feel instant.

The short answer to "how do I stream Claude responses into React" is: call your API with stream: true, read the response body as a stream of SSE events, parse each data: line as JSON, and append the text deltas to a piece of state that your component re-renders on. The rest of this guide covers the details that trip people up — buffering partial chunks, handling abort/cancel, and keeping the UI smooth under fast token rates.

Why streaming matters for React UIs

Without streaming, your component shows a loading spinner for 2-10 seconds, then dumps the full response. Users perceive that as slow even if total latency is identical to streaming. With streaming:

The tradeoff is implementation complexity: you're parsing a live byte stream instead of awaiting a JSON promise. That complexity is manageable with the right hook pattern.

Backend proxy first

Never call the Claude API directly from the browser — your API key would be exposed in network requests. You need a backend route (Node, Next.js API route, Express, etc.) that holds the key and proxies the stream through to the client. This is also where you'd place a gateway like SubToAPI if you want to issue scoped sub_live_... keys per environment instead of sharing a raw Anthropic key across your team. See /docs/streaming for the exact SSE event shapes if you're proxying through SubToAPI.

A minimal Next.js API route proxying a streaming call:

// app/api/chat/route.js
export async function POST(req) {
  const { messages } = await req.json();

  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",
      max_tokens: 1024,
      stream: true,
      messages,
    }),
  });

  return new Response(upstream.body, {
    headers: { "Content-Type": "text/event-stream" },
  });
}

This just forwards the upstream stream byte-for-byte to the browser. No buffering, no parsing on the server — that happens client-side.

The React hook

The core pattern is a useRef for the accumulating text buffer, a useState for triggering re-renders, and a manual SSE line parser since browsers don't have a built-in "fetch as EventSource" API for POST requests.

import { useState, useRef, useCallback } from "react";

export function useStreamingChat() {
  const [text, setText] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);
  const controllerRef = useRef(null);

  const send = useCallback(async (messages) => {
    setText("");
    setIsStreaming(true);
    controllerRef.current = new AbortController();

    const res = await fetch("/api/chat", {
      method: "POST",
      body: JSON.stringify({ messages }),
      signal: controllerRef.current.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 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 {
          // incomplete JSON across chunk boundary — ignore
        }
      }
    }

    setIsStreaming(false);
  }, []);

  const stop = useCallback(() => {
    controllerRef.current?.abort();
    setIsStreaming(false);
  }, []);

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

Key details:

Rendering the component

function Chat() {
  const { text, isStreaming, send, stop } = useStreamingChat();

  return (
    <div>
      <div className="output">{text}</div>
      {isStreaming && <button onClick={stop}>Stop</button>}
      <button
        onClick={() =>
          send([{ role: "user", content: "Explain React suspense in 2 sentences." }])
        }
      >
        Ask
      </button>
    </div>
  );
}

Because text updates on every delta, React re-renders on each token arrival. For very long responses (thousands of tokens), consider throttling setText calls with requestAnimationFrame batching to avoid excessive re-renders — but for typical chat-length outputs this isn't necessary.

Handling tool use and structured events mid-stream

If your prompts trigger tool calls, the stream includes content_block_start/content_block_delta/content_block_stop events with different type values for text vs. tool_use blocks. Your parser needs to branch on event.type and event.content_block.type to distinguish plain text deltas from tool input JSON deltas. See /docs/tools and /docs/messages for the full event schema if you're building anything beyond plain text chat.

Why route through SubToAPI instead of raw Anthropic calls

If you're shipping this to production, you likely want per-environment API keys, usage visibility per team member, and a dashboard instead of managing raw Anthropic credentials across staging/prod. SubToAPI sits in front of your Claude access and gives you sub_live_... keys, the same streaming response format shown above, and team seats so frontend and backend engineers can each have scoped access. Plans start at €9/month on Solo, with Team and Scale tiers for larger engineering groups — check /pricing for details, or jump straight to /docs/quickstart to get a key and test the stream endpoint in minutes.

FAQ

Does the Claude API support streaming out of the box? Yes, pass stream: true in your request and the API responds with server-sent events containing incremental content deltas instead of one JSON blob.

Can I use EventSource in React instead of fetch? EventSource only supports GET requests and can't send custom headers or a JSON body, which most Claude API calls require. Using fetch with a manual ReadableStream reader, as shown above, is the standard workaround for POST-based SSE.

How do I stop a stream mid-response in React? Pass an AbortController signal into your fetch call and call .abort() from a "Stop" button handler. This immediately terminates the connection and lets you set your UI state back to idle.

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 →