-
Notifications
You must be signed in to change notification settings - Fork 135
Description
Hi, thank you for all the work on the router 🙏🏻 I've found an issue during development.
Code created from the Hello World UI router example:
- https://replit.com/@piotrnajda3000/React-Javascript#index.html (sorry for Replit - Codesandbox didn't respect network throttling, and StackBlitz had some other issue...)
- If you prefer a zip with the code instead: repro.zip
- npm i && cd ./src && npm start
Steps to reproduce
- Load the app in
/route: https://react-javascript--piotrnajda3000.replit.app/ - Enable 3G Network throttling to simulate slow lazy-loading
- Two steps in quick succession: switch to "Buckets" and then to "Test", before "Buckets" finishes loading
Expected
"Test" UI (test.jsx) is visible.
Actual
"Buckets" UI (buckets.jsx) is visible.
Additionally, if you try to go to "Buckets" UI and then back to "Test", still, "Buckets" UI will be rendered. This requires a full page reload.
Video reproduction (w/ 3G throttling enabled)
2026-01-08.17-52-01.mp4
Additional notes
1 - If you let "Buckets" load, then switch to "Test", everything works as expected:
2026-01-08.17-54-24.mp4
2 - If we had three routes, e.g. "Buckets", "Documents", "Test", and with the throttling, we let "Buckets" load after navigating to it, then quickly switched between "Documents" and "Test", we'd see the "Documents" UI issue persisting on "Test" route, but it "Buckets" route would correctly load, since it was already pre-loaded.
Workaround
The code for workaround is implemented in the replit/zip. You only need to switch the lazyLoad (uncomment it in replit code).
Details
// Fix for lazy load race condition: track the latest transition
// and discard stale lazy load results
let latestLazyLoadId = 0;
// Now the fix works by:
//
// 1. Tracking a global latestLazyLoadId - incremented each time any lazy load starts
// 2. Each lazy load captures its ID - when the import() starts
// 3. After import completes, check if stale - if myId !== latestLazyLoadId, a newer navigation has started
// 4. Reject stale loads - returns Promise.reject(transition.abort()) which aborts the transition and prevents the stale states from being registered
//
// This ensures that if you click Test → Buckets quickly
// :
// - Test's lazy load gets ID 1
// - Buckets' lazy load gets ID 2
// - When Test's import() resolves, it checks: 1 !== 2, so it aborts itself
// - Only Buckets' lazy load proceeds to register its states
//
// You can test this by throttling to 3G and rapidly switching between routes - only the last clicked route should render.
function createTrackedLazyLoad(importFn) {
return (transition) => {
const myId = ++latestLazyLoadId;
return importFn().then((module) => {
// If a newer navigation started, reject this stale lazy load
if (myId !== latestLazyLoadId) {
return Promise.reject(transition.abort());
}
return module;
});
};
}
const bucketsState = {
name: "app.admin.buckets.**",
url: "/buckets",
lazyLoad: createTrackedLazyLoad(() => import("./buckets.js")),
};
// ... Also apply to testState With workaround implemented:
2026-01-08.18-02-05.mp4
Thank you!