Claude API React Streaming: Frontend Integration Guide
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:
- First token appears in a few hundred milliseconds
- Users can start reading while generation continues
- You can cancel a request mid-stream (useful for "stop generating" buttons)
- Long responses don't block the UI thread waiting for a single large payload
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:
- Buffer the trailing partial line. Network chunks don't align with SSE line boundaries. Splitting on
\nand keeping the last (possibly incomplete) segment for the next read prevents JSON parse failures. - Wrap
JSON.parsein a try/catch. Even with buffering, you'll occasionally get a split mid-chunk under slow connections. Silently skipping and letting the next chunk complete the line is safer than crashing the render loop. - Use
AbortControllerfor cancellation. Calling.abort()on the controller stops the fetch and lets users click a "Stop" button without waiting for the full response.
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.