Claude API Streaming Response React Example
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
- Re-rendering on every token. For long responses this can get janky. If you notice lag, batch updates with
requestAnimationFrameor debounce state writes every ~16ms instead of every chunk. - Not handling stream aborts. Add an
AbortControllerso navigating away or clicking "stop" actually cancels the upstream fetch instead of leaking a connection. - Forgetting
event.typechecks. SSE streams include multiple event types (message_start,content_block_delta,message_stop, etc.). Onlycontent_block_deltacarries text — filtering correctly avoids duplicated or garbled output. - Mixing up SDK streaming with raw SSE. If you're using an SDK client-side (not recommended for browsers due to key exposure), its stream handling differs from raw
fetchparsing shown here.
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.