Replies: 1 comment
|
Hi Ryan, We've been looking at exactly the same problem from React's perspective, as we're starting to see Router as the largest bottleneck to INP by a considerable margin, and are really keen to also get ViewTransitions into the main app. We're already on 19.3.0 and have been running canary for a while now, so know the space fairly well. We've got a POC patch for router (AI generated with a lot of guidance) mixcloud#1 which can remove the useSyncExternalStore call and get ViewTransitions working again, and in our initial tests is green across the board. However I'm fairly sure we must be missing something obvious here, so we haven't shipped it to production. I've been trying to find the time to clear it up and get a PR up here. I'd initially floated it as a RFC here (#8188) however the first version didn't support selectors properly, but that is now fixed up. Reproduction, patches and harness are in mixcloud/router-transitions-poc |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
While working on the Solid v2 adapter I've spent a lot of time in
loadClientRoute/runClientTransaction, and I observed something really about React, so I wanted to raise it as a direction question rather than sit on it.The current model
Today the client pipeline writes location immediately, then runs beforeLoad + loaders as flights on the matches, and only publishes the destination (commitMatches → setMatches) once they resolve. The framework never sees a match with a live flight. Pending presentation is handled by the router's own machinery: offerPending clones, pendingMs/pendingMinMs timers, render acks. The wait lives in an awaited event handler, outside the framework.
What that costs React
Navigations aren't real transitions. Only the final commit goes through startTransition; the loader wait is invisible to React.
useTransition's isPending can't cover aLinkclick, the profiler sees "nothing, then one update," and interruption (a second click superseding the first) runs on router bookkeeping instead of React's own transition-replacement semantics.The pending machinery is hand-rolled Suspense. pendingMs/pendingMinMs/pending offers re-implement what + transitions do natively: fallback throttling, holding the previous UI, commit coordination. That's core code we maintain that the framework already ships.
use() is stable and unused. Matches already carry their flights. If pending matches were published, a route component (or useLoaderData) could read use(match._flight) — per-match granularity, streaming into boundaries, exactly the shape React 19 wants data to flow in.
View Transitions. React's (canary) activates only for updates inside transitions/Suspense. A navigation whose async lives outside React can't ever use it; we wrap document.startViewTransition by hand instead.
This is also a pattern the router has already half-adopted: defer()/ exists precisely because awaiting everything before publish is sometimes wrong — it's the per-value opt-out from the ordering. Publish-first generalizes what defer already does for individual values to the navigation itself, replacing a special case with the default shape. Elsewhere, Next's App Router works this way today (loading.tsx is a Suspense boundary; navigation commits immediately and streams into it) — and TanStack Router's model predates use() being viable, so none of this is a design criticism; the machinery exists because it had to.
Related: #8188 approaches the same goal from the delivery side. Making the state publish land on the transition lane with consistent snapshots. This proposal is complementary: frames fix how a navigation's update lands; publish-first fixes what the transition waits on. Even with frames, the loader wait itself stays outside React; with both, a navigation is a single transition from click to committed swap, and /useTransition/use() all see the real thing.
The proposal
Invert the publish ordering behind a per-framework capability flag (negotiated from the store factory, so nothing changes for anyone by default):
React's default wouldn't change until/unless it opts in; the flag is the whole migration story. The Solid adapter is a natural pilot (it's new enough to have no compat burden), but the reason I'm raising it is that the payoff I keep seeing is on the React side.
The ask
I'm explicitly not proposing this pre-Start-stable. It touches the most sensitive part of core and it should wait for bandwidth. The question is just directional.
Is publish-first something core wants, such that adapter work should aim at it? If yes, I'm happy to write it up properly (and prototype it in the Solid adapter first so React's version lands against a proven shape).
All reactions