Claude API Streaming in a React Native App
If you've tried to stream Claude API responses in a React Native app the way you would in a browser, you've probably hit a wall: fetch() in React Native's JavaScript runtime (Hermes or JSC) doesn't expose a readable stream on the response body. The response.body.getReader() pattern that works perfectly in Next.js or a browser SPA simply isn't available on mobile. You get the full response only after the connection closes, which defeats the purpose of streaming.
The fix is to use XMLHttpRequest instead of fetch, because React Native's XHR implementation does fire progress events with partial response text as chunks arrive over the wire. Combined with manual parsing of Server-Sent Events (SSE) lines, this gives you the same token-by-token UI you'd get on web — just with a different plumbing layer underneath.
Why fetch streaming doesn't work on mobile
Claude's API (and most LLM APIs) stream responses as SSE: a series of data: {...} lines separated by blank lines, terminated by data: [DONE] or an event signaling completion. On the web, you read this incrementally via ReadableStream. React Native's networking stack is built on top of native iOS/Android HTTP clients, and the JS fetch polyfill doesn't surface incremental body reads — it waits for the request to finish, then hands you the complete string.
There's no polyfill that reliably fixes this for fetch across both Hermes and JSC. The practical workaround developers actually ship is XMLHttpRequest, which React Native implements natively with proper onprogress support.
Streaming with XMLHttpRequest and SSE parsing
Here's a minimal, dependency-free implementation that works against any SSE-compatible Claude endpoint, including a proxy like SubToAPI:
function streamClaude(url, apiKey, body, onToken, onDone, onError) {
const xhr = new XMLHttpRequest();
let processedLength = 0;
xhr.open('POST', url);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', `Bearer ${apiKey}`);
xhr.onprogress = () => {
const newText = xhr.responseText.substring(processedLength);
processedLength = xhr.responseText.length;
newText.split('\n').forEach((line) => {
if (!line.startsWith('data: ')) return;
const payload = line.slice(6).trim();
if (payload === '[DONE]') return;
try {
const parsed = JSON.parse(payload);
const delta = parsed?.delta?.text;
if (delta) onToken(delta);
} catch {
// partial JSON chunk straddling a progress event, ignore and wait
}
});
};
xhr.onload = () => onDone();
xhr.onerror = (e) => onError(e);
xhr.send(JSON.stringify(body));
return xhr; // keep a reference if you need to call xhr.abort()
}
A few things to watch for:
- Partial JSON lines.
onprogressfires on arbitrary network buffer boundaries, not clean SSE event boundaries. Adata:line can be split across two progress events. WrappingJSON.parsein a try/catch and re-buffering the remainder (rather than discarding it) is safer for production — the snippet above is simplified for clarity. - Backpressure on Android. Some Android HTTP client configurations buffer more aggressively than iOS, so you may see fewer, larger progress events on Android emulators. Test on a physical device before concluding streaming "isn't working."
- Abort handling. Store the
xhrreference and callxhr.abort()when the user navigates away or cancels generation, otherwise the connection and the component callback keep running after unmount.
Using a proxy instead of calling Anthropic directly
Two separate problems come up once you move past a prototype: where you store the Anthropic API key (never in the app bundle), and how you track usage per user or per feature. This is the point where most teams shipping a real product put a thin backend or API gateway between the app and the model provider rather than embedding a raw ANTHROPIC_API_KEY in React Native.
SubToAPI is built for exactly this: it turns your existing Claude access into an HTTPS API with its own scoped keys (sub_live_...), so your mobile app authenticates against SubToAPI instead of holding Anthropic credentials. The streaming contract is standard SSE over HTTPS, so the XHR pattern above works unchanged — you just point the URL and bearer token at SubToAPI:
const xhr = streamClaude(
'https://api.subtoapi.app/v1/messages',
process.env.SUBTOAPI_KEY,
{
model: 'claude-opus-4-5',
max_tokens: 1024,
stream: true,
messages: [{ role: 'user', content: 'Explain backpressure in streaming APIs' }],
},
(token) => setReply((prev) => prev + token),
() => setLoading(false),
(err) => setLoading(false)
);
What changes under the hood: SubToAPI handles key rotation, per-key usage metadata, and team seat separation, so you're not shipping a shared secret inside your app binary and hoping nobody decompiles it. Full request/response shape is in the docs, the streaming contract specifically in /docs/streaming, and a working end-to-end example in the quickstart. Pricing starts at Solo for €9/month, with Team and Scale tiers for multi-seat setups — see /pricing if you're deciding between a direct Anthropic key and a managed layer.
Rendering tokens smoothly in the UI
Once tokens are arriving via onToken, avoid calling setState on every single character chunk if your model emits very granular deltas — batch updates with a small debounce (16–50ms) tied to a requestAnimationFrame-style tick, or you'll see dropped frames on lower-end Android devices during long generations. A FlatList or Text component re-rendering on every token works fine for short replies but will visibly stutter on multi-paragraph outputs.
If your app also needs tool calls mid-stream (function calling results interleaved with text), the SSE events carry distinct content_block_start / content_block_delta types — parse the type field before assuming every event is a text delta. See /docs/tools and /docs/messages for the exact event shapes if you're streaming through SubToAPI.
FAQ
Can I use fetch for Claude streaming in React Native at all? Not reliably for incremental reads. fetch will complete the request and only then give you the full body. XMLHttpRequest with onprogress is the practical option on both iOS and Android today.
Does Expo support streaming the same way as bare React Native? Yes — Expo apps run on the same JS engine and networking layer, so the XHR approach works identically in managed and bare workflows. No native module is required.
Do I need a backend, or can the app call Claude directly? Technically you can call Anthropic directly from the client, but it means embedding your API key in the app bundle, which is a real security risk once the app is distributed. A thin gateway like SubToAPI or your own backend keeps the key server-side while your app authenticates with a scoped, revocable key instead.