LLM chat streaming
Three conversations with a mock LLM. Each reply streams in token by token, and all three can stream at the same time — but only the chat you’re looking at is on screen.
Things to try:
- Open a chat and watch the reply stream in. Later tokens update in place — the Suspense fallback only ever shows before the first token.
- Switch to another chat mid-stream, wait a moment, and switch back: the first chat kept streaming while hidden and reveals instantly, fully caught up.
- Hover a chat you haven’t opened yet before clicking it: the reply starts streaming in the background, so opening it skips the fallback entirely.
The mock vs. your code
llm.ts is the only mock in this demo: a stand-in for a real streaming LLM API that emits token deltas as an observable. In a real app it would wrap a fetch ReadableStream or an SSE connection. Everything else — chat.ts and App.tsx — is what your own code would look like.
The userland recipe is small:
scanfolds tokens into the reply. The conversation stream emits the whole message list on every token, so components just render the latest value.shareReplay({bufferSize: 1, refCount: false})makes the stream independent of who’s watching. The reply keeps streaming while its chat is hidden or unmounted, and any subscriber — new or returning — immediately gets the latest state. The source completes when the reply ends, so nothing leaks.
Why the React side “just works”
useObservablePromise+use()suspend until the first emission and then update in place. Streaming tokens never re-trigger the fallback — that’s the semantic difference from re-fetch-per-render approaches.<Activity mode="hidden">keeps visited chats mounted with their state intact. Hiding a chat tears down its live subscription (like an effect), but the shared conversation stream keeps running, and on reveal the hook synchronously reads the current snapshot — no flash, no refetch, all the tokens that arrived meanwhile.preloadObservablePromisewarms the same cache the hook reads from, outside of render. Hover-to-preload is one line on the button.
See Activity and preload for a side-by-side comparison of prefetch strategies, and Suspense data fetching for the promise semantics on their own.
Last updated on