RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Articles/Dev Tools
Dev Tools/2026-04-13Advanced

LLM Streaming in Rork Apps: Building ChatGPT-Style Real-Time AI Responses with Expo and SSE

Field notes on shipping LLM streaming (SSE) in React Native and Expo. Anthropic, OpenAI and Gemini behind one interface, AbortController cancellation and retries, a Cloudflare Workers proxy, context compaction, and a mock SSE server for testing at zero API cost.

streaming2SSELLM3AI29Anthropic4OpenAI5React Native234Expo194Cloudflare Workers24Rork548

Premium Article

Twelve seconds of a spinner. That was the first version of the AI chat in my app: tap send, watch a circle rotate, and by the time the answer finally landed the tester's thumb was already reaching for the home button. The model wasn't slow. The design was — waiting for the full generation before showing anything turns the entire wait into dead air.

LLM streaming, powered by Server-Sent Events (SSE), solves this at the source. Instead of waiting for the model to finish generating, tokens stream to the client as they're produced. Users see text flowing within milliseconds, and that psychological shift — from "waiting" to "watching it think" — has a measurable impact on retention.

The catch is that streaming in React Native requires a fundamentally different approach than the browser. EventSource doesn't exist in the RN runtime. Reading a ReadableStream from fetch requires specific handling. Wiring up AbortController correctly is trickier than it looks. This guide covers all of it, with production-tested code you can drop into your Rork project.

Why Streaming Is a UX Inflection Point

Before diving into implementation, it's worth understanding what's actually happening from a user perspective.

With blocking (non-streaming) responses, the pattern is always the same: send a message, sit through silence, receive a wall of text. Short answers survive it. The longer, more considered responses that make an LLM worth paying for do not — asking Claude Sonnet for a few hundred words routinely put me in the 15–20 second range.

You don't need analytics to see what that does. Watch someone use it. Around five seconds they glance away from the screen. Around ten they switch apps. When they come back they've forgotten what they asked. The conversation stops before any retention metric has a chance to record it.

Streaming collapses this experience. The first token arrives in under a second. The user's cognitive state shifts from passive waiting to active reading. Even if the total generation time is identical, the perceived speed is dramatically different.

There's also a practical cost angle. When you implement streaming with proper cancellation, users who get enough of an answer midway can stop generation. That cuts API costs on incomplete requests. Depending on your use case, this can reduce monthly LLM spend by 30–50%.

The Core Challenge: React Native Has No EventSource

If you've implemented SSE in a web app, your first instinct might be to reach for EventSource:

// Works in browsers — fails silently in React Native
const es = new EventSource('https://api.anthropic.com/v1/messages');
es.onmessage = (e) => console.log(e.data);

In React Native, this throws or does nothing depending on your setup. The EventSource Web API simply isn't part of the RN runtime.

Streaming means reading a ReadableStream off a fetch() response. And this is where the second trap sits — the one that cost me half a day.

React Native's global fetch does not give you response.body. RN's fetch is a polyfill layered on XMLHttpRequest, and stream support was never implemented (facebook/react-native#27741 has been open for years). Upgrading your Expo SDK does not change this.

What you want is expo/fetch, added in Expo SDK 52. It is a separate WinterCG-compliant implementation that you pull in as a named import. It does not replace the global, so if you forget the import line your code quietly falls back to the old implementation — and that is genuinely hard to spot.

// ❌ The global fetch — the request succeeds, but body is undefined
const bad = await fetch(url, { method: 'POST', body });
bad.body; // → undefined (RN polyfill)
 
// ✅ Expo SDK 52+. The named import is not optional
import { fetch } from 'expo/fetch';
 
const response = await fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'anthropic-version': '2023-06-01',
    'x-api-key': 'YOUR_API_KEY', // Use a proxy in production — more on this below
  },
  body: JSON.stringify({
    model: 'claude-haiku-4-5-20251001',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'Hello' }],
    stream: true,
  }),
});
 
const reader = response.body?.getReader();
const decoder = new TextDecoder();
 
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value, { stream: true });
  // Parse SSE events from chunk
}

When response.body comes back as undefined, it is almost always one of three causes.

SymptomCauseFix
body is undefined, no error thrownYou are on the global fetchAdd import { fetch } from 'expo/fetch';
Still undefined after importingExpo SDK 51 or earlierUpgrade to SDK 52+, or fall back to a library such as react-native-sse
Works on web, fails on device onlyEXPO_PUBLIC_USE_RN_FETCH=1 is setThat flag restores the RN implementation as the global. Named imports are unaffected, so check your import path first

Rork scaffolds projects on a recent SDK, but whether the generated code actually calls expo/fetch depends on what it wrote for you. When adding streaming, read the import line with your own eyes before anything else.

Parsing SSE Format Correctly

The SSE format that Anthropic sends looks like this:

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":", world"}}

event: message_stop
data: {"type":"message_stop"}

OpenAI uses a slightly different format:

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"},"index":0}]}

data: [DONE]

A critical detail: one reader.read() call can return multiple SSE events, and a single event can span across multiple reads. If you parse each chunk directly without buffering, you'll see intermittent JSON parse errors in production that are maddeningly hard to reproduce.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Developers stuck on SSE fetch implementation in React Native can now run LLM streaming in production starting today
You'll get copy-paste-ready code that covers AbortController cancellation, error retries, and API cost optimization as a complete set
You'll learn a multi-provider abstraction pattern covering Anthropic, OpenAI, and Gemini — enabling provider switching at runtime without a single line of app code change
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
Dev Tools2026-07-30
What Renovate may bump in an Expo app, and what it must never touch
Turning on automated dependency updates in a Rork-generated app also hands Renovate the 123 packages Expo SDK 57 pins. Measured on 2026-07-30, six of them sit a full major version behind npm latest. Here is how to generate the ignore list from the SDK instead of maintaining it by hand.
Dev Tools2026-07-28
Counting what prebuild --clean will erase before you upgrade to Expo SDK 57
A raw diff between two generated ios/ trees showed 649 changed lines; only 3 were real edits. How to count what prebuild --clean erases, and move it into a config plugin.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →