Async React demo
The final demo from Rick Hanlon’s React Conf 2025 Async React talk (rickhanlonii/async-react , live at async-react.dev ), rebuilt on react-rx + RxJS.
The talk’s thesis: product code stays simple and declarative when three things hold. Routing runs in transitions. Data fetching suspends by default. And design components own their pending/optimistic feedback through action props. The app then adapts to network speed on its own.
Things to try (open the network debugger at the bottom of the preview):
- Fast network (all delays at 0): log out and back in, switch tabs, search, toggle lessons. Nothing ever looks “loading”. The pending shimmers technically exist, but a CSS
animation-delaykeeps them invisible for 300ms (1.5s on search). Fast actions finish first. - Slow down
/lessons(~1500ms): tab switches now shimmer the optimistic tab. Searching shimmers the input while your keystrokes appear immediately (useOptimistic). The list only shows skeletons on first load. Transitions keep old results visible for every later change. - Slow down
/loginand/lessons, then log out and in: the login button stays pending through the POST and the prefetch. Under 1s of lessons latency, you land on a fully-loaded home screen. Above it, login navigates anyway and the Suspense fallback takes over. That’s thePromise.raceinprefetchLessons. - Slow down
/lesson/:id/toggleand complete a lesson on the “In progress” tab: the checkmark flips instantly (optimistic). The button shimmers after 300ms. When the mutation and refetch land, the item leaves the list. Updated in place, no fallback.
What react-rx changes
The React side is untouched from the original. Same useTransition, useOptimistic, and <Suspense> patterns. Same action-prop design components. Same transition router.
The data layer is where streams take over (api.ts):
- Revalidation can’t cause fallbacks. The original pairs a cache-of-promises with
revalidate(): clear everything, re-render, re-fetch. Here,lessons$(tab, search)is a cached observable that re-fetches whenrevalidate$fires.useObservablePromiseupdates in place after the first emission, so a revalidation can never re-trigger a Suspense fallback. The original had to engineer around that risk. - Prefetching is one call.
prefetchLessons()ispreloadObservablePromiseraced against a 1s timeout. It warms the same promise the home screen’s hook reads. - Stale-while-revalidate for free. Returning to a previously-seen tab replays the last result instantly, while a fresh fetch streams in behind it. That’s
shareReplayplusstartWith. - The network debugger is streams end-to-end. The delay knobs are
BehaviorSubjects the fetch layer reads. The request log is onescanover request events, read withuseObservable.
Route state deliberately stays in React state. Only React state updates can be marked as transitions, and that is exactly what navigation wants. Streams for server data, transitions for view state. Each tool where it’s strongest.
One omission: the original animates with React’s <ViewTransition>, which is still experimental-only. This port runs on stable React, so those animations are left out.