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
{{ message }}
Repository navigation
A reader that mounts while React holds a navigation reads the destination's URL and params (usePathname, useSearchParams, useParams, useSegments) #1046
A component that first mounts on the page being left, after a navigation's response has arrived but while React still holds the destination, reads the destination's URL and params from usePathname(), useSearchParams(), useParams() and useSegments(). Components that were already mounted keep the values of the page they are on. It happens on a push and on Back/Forward.
Found while working on #1031. useLocationState does not have this since #1033.
Evidence
Browser, load-more fixture, a push whose loader is held, both apps, dev and production:
the list on screen: page 1
readers already mounted: /load-more?page=1 (their own entry)
a reader mounted during the hold: /load-more?page=2&cold=1 (the destination)
Unit, through renderRoute, a held push on /items/:id with a transition:
mounted reader: /items/1?tab=a, params.id=1
reader mounted during the hold: /items/2?tab=b, params.id=2
Cause
The location and params are written to the event controller at the navigation transaction's commit, when the response arrives (src/browser/navigation-transaction.ts, commit()). Mounted readers pick the change up in the payload's batch, which React holds together with the destination tree. A reader that mounts in between initializes from the controller's current value, which is already the destination's.
useNavigation().location has the related property that it moves at the transaction's commit, ahead of a held tree.
Who hits it
A page that mounts something new while a navigation is pending (a virtualized row, a dialog, a late Suspense reveal) where that component reads the URL hooks or useParams. Narrow, and the window is only the time React holds the tree after the response arrived (a destination that suspends outside a Suspense boundary, a same-route hold, transition()).
Direction
Hold the pathname, search and params of the entry on screen next to its location state in NavigationProvider, read through context, the way #1033 did for location state. Then a value can only render with its entry's tree, for every reader. Estimated 150 to 250 lines of source (three hooks, useSegments, ssr-root.tsx, renderRoute), one context per value to keep the render-count pins, with 858 B of headroom under the client chunk limit. Not measured.
This would also be the place to settle item 1 of #1042 (a commit without a payload landing while another navigation is held), which reproduces: a revalidate: false push during a held push puts the shallow entry's URL and location state on the page being left within about 15 ms, for the rest of the hold.
Assessment after an independent review: real, low value, parked. The expected-failure pin in navigation-url-commit.test.ts stays, so the suite turns red if this gets fixed by something else.
What has to be true for a user to see it
The navigation's commit is held: a transition() segment, a same-route navigation whose loaders are still streaming (the default hold), an action or stale revalidation, or a destination that suspends outside a Suspense boundary. A cross-route navigation that commits at once has no window.
The response has started to arrive (the transaction has committed). Before that every reader holds the old URL.
A component first MOUNTS in the page being left during that window and reads usePathname / useSearchParams / useParams / useSegments.
The window can be most of the navigation (from the first byte until the slowest streamed loader settles), so it is not short. What is rare is condition 3. The realistic case: a product page to product page navigation, the user opens a drawer on the old product during the wait, and the drawer reads useParams().productId to build its action; it then acts on the destination product while the old one is shown.
Same root cause, seen during #1049: a page mounted by one push while the next push had already committed showed the next entry's URL over its own content (1 of 6 dev runs with back-to-back navigations), and item 1 of #1042.
If it is built
The context design in the description is the only sound one. The alternatives were checked and rejected: hooks that initialize from a provider snapshot but keep the controller subscription stay stale after the commit (nothing notifies at React's commit); a useSyncExternalStore read of the controller's location makes mounted readers move at the transaction's commit, undoing #1049 for the held part; a "presented location" set from a layout effect costs an extra render and adds a second source.
With one context per value (pathname, search, params) the hooks lose their subscription code, so the estimate is closer to 100 to 150 changed lines with a roughly flat bundle; not measured. It does not settle #1042 item 1 on its own: a shallow push during a held push is an urgent update and still lands on the page on screen. The simple rule there is to document that a shallow push's URL applies at once to whatever is on screen, which is today's behavior.
Closing as documented behaviour, by the owner's decision C on the hooks review (rango-t96). A component that first mounts while React holds a transition() page reads the destination's URL and params. Components already mounted keep the page on screen's values until it commits, and then every hook agrees. A fix would add a second path to each hook's value, and a render after paint on every fully prefetched click. The behaviour is pinned in packages/rangojs-router/src/testing/tests/render-route-late-reader-commit.test.tsx and documented in the hooks skill and execution-model.md ("Client Route-State Commits").
Activity
fix(router): on back/forward, usePathname and useSearchParams change …
ivogt commentedon Oct 4, 2026
Assessment after an independent review: real, low value, parked. The expected-failure pin in
navigation-url-commit.test.tsstays, so the suite turns red if this gets fixed by something else.What has to be true for a user to see it
transition()segment, a same-route navigation whose loaders are still streaming (the default hold), an action or stale revalidation, or a destination that suspends outside a Suspense boundary. A cross-route navigation that commits at once has no window.usePathname/useSearchParams/useParams/useSegments.The window can be most of the navigation (from the first byte until the slowest streamed loader settles), so it is not short. What is rare is condition 3. The realistic case: a product page to product page navigation, the user opens a drawer on the old product during the wait, and the drawer reads
useParams().productIdto build its action; it then acts on the destination product while the old one is shown.Same root cause, seen during #1049: a page mounted by one push while the next push had already committed showed the next entry's URL over its own content (1 of 6 dev runs with back-to-back navigations), and item 1 of #1042.
If it is built
The context design in the description is the only sound one. The alternatives were checked and rejected: hooks that initialize from a provider snapshot but keep the controller subscription stay stale after the commit (nothing notifies at React's commit); a
useSyncExternalStoreread of the controller's location makes mounted readers move at the transaction's commit, undoing #1049 for the held part; a "presented location" set from a layout effect costs an extra render and adds a second source.With one context per value (pathname, search, params) the hooks lose their subscription code, so the estimate is closer to 100 to 150 changed lines with a roughly flat bundle; not measured. It does not settle #1042 item 1 on its own: a shallow push during a held push is an urgent update and still lands on the page on screen. The simple rule there is to document that a shallow push's URL applies at once to whatever is on screen, which is today's behavior.
ivogt commentedon Oct 28, 2026
Closing as documented behaviour, by the owner's decision C on the hooks review (rango-t96). A component that first mounts while React holds a transition() page reads the destination's URL and params. Components already mounted keep the page on screen's values until it commits, and then every hook agrees. A fix would add a second path to each hook's value, and a render after paint on every fully prefetched click. The behaviour is pinned in packages/rangojs-router/src/testing/tests/render-route-late-reader-commit.test.tsx and documented in the hooks skill and execution-model.md ("Client Route-State Commits").