You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During rapid successive navigations (e.g., user clicks A→B→C quickly), the isSameRoute check in app-browser-entry.ts compares against stale window.location.pathname because URL commits are deferred via useLayoutEffect.
Same-route (search param change) during cross-route transition
Uses sync updates instead of smooth startTransition
Cross-route during another pending navigation
May incorrectly use startTransition (rare but possible)
User-facing: Janky, non-smooth transitions when rapidly clicking between routes or changing filters during page loads.
Code Reference
This was acknowledged as a known limitation in PR #690 with this comment (lines 621-625):
// NB: During rapid navigations, window.location.pathname may not reflect// the previous navigation's URL yet (URL commit is deferred). This could// cause misclassification (synchronous instead of startTransition or vice// versa), resulting in slightly less smooth transitions but correct behavior.
This issue tracks implementing a proper fix rather than accepting the workaround.
Root Cause
The ClientNavigationState tracks pending params but not pending pathname:
typeClientNavigationState={// ...clientParams: Record<string,string|string[]>;pendingClientParams: Record<string,string|string[]>|null;// ✅ TrackedcachedPathname: string;// ❌ Only committed, no pending pathname// ...}
Proposed Solution
Option A: Track Pending Pathname (Recommended)
Add pendingPathname to ClientNavigationState in navigation.ts:
Clear pendingPathname in commitClientNavigationState() after successful commit
Handle error cases: clear on navigation failure/supersession
Complexity: Requires restructuring navigation flow because isSameRoute is evaluated beforestageClientParams() in both cached and non-cached paths.
Option B: Track In-Flight Navigation Destination
Add a destinationPathname field set immediately when navigation starts, before any async work. This separates "where we are going" from "params we are staging".
Edge Cases to Handle
Overlapping navigations (A→B→C): When C starts, should compare against B (most recent pending), not A. The navId/activeNavigationId system already supersedes older navigations.
Navigation superseded before commit: If B→C starts but is superseded by B→D, pending pathname should update to D. Cleanup must be robust.
Error during navigation: If navigation fails, pendingPathname must be cleared in catch blocks. Current _snapshotPending pattern shows the complexity of cleanup.
Back/forward (traverse) navigation: Different code path via popstate handler. Uses window.location.href directly, less affected.
Files Affected
packages/vinext/src/shims/navigation.ts — add pendingPathname to state type, update commitClientNavigationState()
Problem
During rapid successive navigations (e.g., user clicks A→B→C quickly), the
isSameRoutecheck inapp-browser-entry.tscompares against stalewindow.location.pathnamebecause URL commits are deferred viauseLayoutEffect.Current Behavior (lines 619-622)
Race condition window:
Navigation 1 starts (A→B):
renderNavigationPayload()called withisSameRoute=false(correctly: B ≠ A)navigationCommitEffectqueues URL update viapushHistoryStateWithoutNotify()window.locationNavigation 2 starts (B→C) before Navigation 1 commits:
isSameRoutecomparesurl.pathname(C) withwindow.location.pathname(still A)Navigation 1 eventually commits:
commitClientNavigationState()callssyncCommittedUrlStateFromLocation()state.cachedPathnameupdates to BisSameRoutewas already evaluated incorrectlyImpact
startTransitionstartTransition(rare but possible)User-facing: Janky, non-smooth transitions when rapidly clicking between routes or changing filters during page loads.
Code Reference
This was acknowledged as a known limitation in PR #690 with this comment (lines 621-625):
This issue tracks implementing a proper fix rather than accepting the workaround.
Root Cause
The
ClientNavigationStatetracks pending params but not pending pathname:Proposed Solution
Option A: Track Pending Pathname (Recommended)
Add
pendingPathnametoClientNavigationStateinnavigation.ts:Flow changes:
pendingPathnameimmediately when__VINEXT_RSC_NAVIGATE__startsisSameRoutecomparison:pendingPathnameincommitClientNavigationState()after successful commitComplexity: Requires restructuring navigation flow because
isSameRouteis evaluated beforestageClientParams()in both cached and non-cached paths.Option B: Track In-Flight Navigation Destination
Add a
destinationPathnamefield set immediately when navigation starts, before any async work. This separates "where we are going" from "params we are staging".Edge Cases to Handle
Overlapping navigations (A→B→C): When C starts, should compare against B (most recent pending), not A. The
navId/activeNavigationIdsystem already supersedes older navigations.Navigation superseded before commit: If B→C starts but is superseded by B→D, pending pathname should update to D. Cleanup must be robust.
Error during navigation: If navigation fails,
pendingPathnamemust be cleared in catch blocks. Current_snapshotPendingpattern shows the complexity of cleanup.Back/forward (traverse) navigation: Different code path via
popstatehandler. Useswindow.location.hrefdirectly, less affected.Files Affected
packages/vinext/src/shims/navigation.ts— addpendingPathnameto state type, updatecommitClientNavigationState()packages/vinext/src/server/app-browser-entry.ts— updateisSameRoutelogic, set/clear pending pathnameWhy This Matters Now
UX regression: Rapid navigation is a common user pattern. The current workaround produces visible jank.
Self-contained fix: ~30 lines changed, does not depend on major refactors like Layout Persistence #726 (layout persistence architecture).
Code health: The comment acknowledges this as debt. Better to fix properly than let it linger.
Related
Testing Needed
isSameRouteclassification with simulated pending navigation