-
Notifications
You must be signed in to change notification settings - Fork 292
fix(navigation): track pending pathname to fix isSameRoute race condition during rapid navigation #745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(navigation): track pending pathname to fix isSameRoute race condition during rapid navigation #745
Changes from all commits
78eefbf
76101bb
7056e6a
5c9e3d6
391a244
61734a1
9287f4b
3cbab5a
b35fc90
4f694b6
6299b3d
cbd5049
44d466e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -474,6 +474,8 @@ type ClientNavigationState = { | |||||||||||||
| clientParamsJson: string; | ||||||||||||||
| pendingClientParams: Record<string, string | string[]> | null; | ||||||||||||||
| pendingClientParamsJson: string | null; | ||||||||||||||
| pendingPathname: string | null; | ||||||||||||||
| pendingPathnameNavId: number | null; | ||||||||||||||
| originalPushState: typeof window.history.pushState; | ||||||||||||||
| originalReplaceState: typeof window.history.replaceState; | ||||||||||||||
| patchInstalled: boolean; | ||||||||||||||
|
|
@@ -486,7 +488,7 @@ type ClientNavigationGlobal = typeof globalThis & { | |||||||||||||
| [_CLIENT_NAV_STATE_KEY]?: ClientNavigationState; | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| function getClientNavigationState(): ClientNavigationState | null { | ||||||||||||||
| export function getClientNavigationState(): ClientNavigationState | null { | ||||||||||||||
| if (isServer) return null; | ||||||||||||||
|
|
||||||||||||||
| const globalState = window as ClientNavigationGlobal; | ||||||||||||||
|
|
@@ -499,6 +501,8 @@ function getClientNavigationState(): ClientNavigationState | null { | |||||||||||||
| clientParamsJson: "{}", | ||||||||||||||
| pendingClientParams: null, | ||||||||||||||
| pendingClientParamsJson: null, | ||||||||||||||
| pendingPathname: null, | ||||||||||||||
| pendingPathnameNavId: null, | ||||||||||||||
| // NB: These capture the currently installed history methods, not guaranteed | ||||||||||||||
| // native ones. If a third-party library (analytics, router) has already patched | ||||||||||||||
| // history methods before this module loads, we intentionally preserve that | ||||||||||||||
|
|
@@ -729,6 +733,34 @@ export function getClientParams(): Record<string, string | string[]> { | |||||||||||||
| return getClientNavigationState()?.clientParams ?? _fallbackClientParams; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Set the pending pathname for client-side navigation. | ||||||||||||||
| * Strips the base path before storing. Associates the pathname with the given navId | ||||||||||||||
| * so only that navigation (or a newer one) can clear it. | ||||||||||||||
| */ | ||||||||||||||
| export function setPendingPathname(pathname: string, navId: number): void { | ||||||||||||||
| const state = getClientNavigationState(); | ||||||||||||||
| if (!state) return; | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still unaddressed from both prior reviews:
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Flagged in all three prior review rounds and still unaddressed.
Suggested change
|
||||||||||||||
| state.pendingPathname = stripBasePath(pathname, __basePath); | ||||||||||||||
| state.pendingPathnameNavId = navId; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Clear the pending pathname, but only if the given navId matches the one | ||||||||||||||
| * that set it, or if pendingPathnameNavId is null (no active owner). | ||||||||||||||
| * This prevents superseded navigations from clearing state belonging to newer navigations. | ||||||||||||||
| */ | ||||||||||||||
| export function clearPendingPathname(navId: number): void { | ||||||||||||||
| const state = getClientNavigationState(); | ||||||||||||||
| if (!state) return; | ||||||||||||||
| // Only clear if this navId is the one that set the pendingPathname, | ||||||||||||||
| // or if pendingPathnameNavId is null (no owner) | ||||||||||||||
| if (state.pendingPathnameNavId === null || state.pendingPathnameNavId === navId) { | ||||||||||||||
| state.pendingPathname = null; | ||||||||||||||
| state.pendingPathnameNavId = null; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function getClientParamsSnapshot(): Record<string, string | string[]> { | ||||||||||||||
| return getClientNavigationState()?.clientParams ?? _EMPTY_PARAMS; | ||||||||||||||
| } | ||||||||||||||
|
|
@@ -888,7 +920,7 @@ function withSuppressedUrlNotifications<T>(fn: () => T): T { | |||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| export function commitClientNavigationState(): void { | ||||||||||||||
| export function commitClientNavigationState(navId?: number): void { | ||||||||||||||
| if (isServer) return; | ||||||||||||||
| const state = getClientNavigationState(); | ||||||||||||||
| if (!state) return; | ||||||||||||||
|
|
@@ -908,6 +940,13 @@ export function commitClientNavigationState(): void { | |||||||||||||
| state.pendingClientParams = null; | ||||||||||||||
| state.pendingClientParamsJson = null; | ||||||||||||||
| } | ||||||||||||||
| // Clear pending pathname when navigation commits, but only if: | ||||||||||||||
| // - The navId matches the one that set pendingPathname | ||||||||||||||
| // - No newer navigation has overwritten pendingPathname (pendingPathnameNavId === null or matches) | ||||||||||||||
| if (state.pendingPathnameNavId === null || state.pendingPathnameNavId === navId) { | ||||||||||||||
| state.pendingPathname = null; | ||||||||||||||
| state.pendingPathnameNavId = null; | ||||||||||||||
| } | ||||||||||||||
| const shouldNotify = urlChanged || state.hasPendingNavigationUpdate; | ||||||||||||||
| state.hasPendingNavigationUpdate = false; | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
commitClientNavigationState()call (withoutnavId) runs when_snapshotPendingis true in the outer catch. It happens to be safe becausecommitClientNavigationState(undefined)won't match anypendingPathnameNavIdthat's a number — the conditionstate.pendingPathnameNavId === navIdwill benumber === undefinedwhich isfalse.But this is fragile and relies on an implicit type mismatch. If someone later changes the
navIdparameter type or the clearing condition, this could silently start clobbering pending pathnames from other navigations.Since
navIdis available in scope here, it would be safer and more consistent to pass it:This makes the intent explicit and matches the pattern used in
createNavigationCommitEffect,renderNavigationPayload, andupdateBrowserTree.