-
Notifications
You must be signed in to change notification settings - Fork 7
Integrating the new Navigation API into RSC framework #11
I've been trying to integrate the new Navigation API into my RSC framework. You can see my progress in the vid below where I first cancel a navigation by pressing 'X' and then change my mind mid-navigation. Concurrent React pairs very nicely with navigation interception. Up to a point.
Screen.Recording.2026年05月19日.at.18.17.31.mov
You see, the new Navigation api is meant to work as shown below. You fetch the data in the precommitHandler and update the DOM in the handler. The precommitHandler runs before the URL updates and the user can cancel the navigation during this phase. Once the precommitHandler resolves then the browser records the scroll position and may take a screen snapshot to display during a swipe back transition (on mobile, for example).
const onNavigate = (e) => { e.intercept({ async precommitHandler() { // fetch data }, async handler() { // update the DOM } }); };
The problem is with concurrent React I can only get it working as follows. So React commits the DOM before the precommitHandler resolves and this messes up scroll restoration and screen snapshots, for example.
const onNavigate = (e) => { e.intercept({ async precommitHandler() { // fetch data and update the DOM }, }); };
Would React be interested in exposing something to RSC frameworks so they can get in between the background render completing and the changes being committed to the DOM?
All reactions
Replies: 3 comments 5 replies
I believe Next.js has managed to integrate this API, maybe worth digging through it's source?
All reactions
What makes you think that? There's no mention of precommitHandler in their codebase
All reactions
Ah I think I got mixed up with react/react#33162 maybe
All reactions
Wow, thanks for sending it my way 🙏 . It's ingenious, but they are cheating a bit. They're faking a navigation to show the native progress. I don't see them supporting pressing the 'X' to cancel, but you never know.
My favourite usecase for the new Navigation API is delaying the browser back/forward. The browser used to force you to show a cached page for history navigation because it updated the URL instantly (I think that's why Next.js felt entitled to add caching everywhere else as well 😆).
But that's not true anymore as you can see in the vid below. I press the back button, the browser instantly updates the arrows and progress button, but it doesn't change the URL. Then I cancel the back and the browser reverts the arrows and progress button.
Even better, you never end up showing stale data. In the vid you can see I update the person's name then navigate back and the name is updated in the list. There's no flicker because the history back waits until the new data is fetched.
Screen.Recording.2026年05月21日.at.17.50.53.mov
All reactions
Switching from useEffect to useInsertionEffect seems to fix the timing issue. I've tested the swipe back transition on Android Chrome and it shows the correct snapshot.
let resolve; const onNavigate = (e) => { e.intercept({ async precommitHandler() { return new Promise(res => { resolve = res; }) }, }); }; useInsertionEffect(() => { resolve(); });
All reactions
Unfortunately, seems it's still too late for scroll restoration but could probably do that manually
All reactions
A demo of the current behavior would help understand the issue. Ideally a cloneable repo that's also deployed and testable on a public URL.
All reactions
@eps1lon I've finished the work of adding precommit handling to the Navigation router.
I've built a demo repo for you to take a look at.
The key advantages of using precommit handling are
- Google Chrome shows the 'X' progress indicator for all navigations just like in MPA. Pressing the 'X' cancels the navigation.
- The browser history back never shows stale data. Select a person, edit their name then press browser back and the list will show their new name.
Screen.Recording.2026年06月28日.at.10.51.51.mov
React concurrent rendering fits surprisingly well with the precommit handling from the new navigation api. But there are points of friction.
- In Google Chrome, when the user presses 'X' to cancel the navigation, the browser kills the RSC stream. The only way to prevent the error reaching React was to wrap in a custom
ReadableStreamand not pass on the chunk when signal aborts. Is there a better way? - There's a timing mismatch between precommit handling and concurrent React. The browser expects that when the precommit handler completes the UI will still be on the old page. This is when the browser stores the scroll position, for example. But React doesn't tell libraries that a background render is complete until it's committed to the DOM. This is too late for scroll restoration. Can React notify RSC routers before it commits?
- Google Chrome on mobile takes a snapshot of the previous page so that it can show it when the user swipes back. Committing the navigation in
useEffectis too late because the browser will take a snapshot of the current page instead of the previous one. But worked around this by committing the navigation inuseInsertionEffectinstead (this is still too late to fix scroll restoration). Is this just luck or does React guarantee the timing gap betweenuseInsertionEffectanduseEffect(it seemed to increase between React 19 and 19.2, for example)?
- Google Chrome on mobile takes a snapshot of the previous page so that it can show it when the user swipes back. Committing the navigation in
Here's the rough picture of how the Navigation router matches up concurrent rendering with precommit handling. Start a browser navigation but intercept it and hold onto the returned promise's resolve. Start the React background render. When React commits the background render call resolve to commit the browser navigation.
let commit; // 2. Intercept the navigation window.navigation.addEventListener('navigate', e => { e.intercept({ async precommitHandler() { return new Promise(res => { // 3. Hold onto the resolve commit = res; }); } }); }); // 1. Perform the navigation window.navigation.navigate(url) // 4. Start the React background render startTransition(() => { stateNavigator.navigate(url); }); // 5. Commit the navigation when React commits the background render useInsertionEffect(() => { commit(); });