Repository navigation
Description
Problem
On a client navigation, a mounted useLocationState reader receives the DESTINATION entry's state before the destination tree commits. For as long as the navigation is held (a loader is pending), the page shows the new entry's location state next to the old entry's data.
The visible case is a "load more" list: ?page=N loads that page's items through a loader, and the items already on screen travel to the next page as location state on the <Link>. After the click, the list re-renders with the carried items of pages 1..N while its loader data is still page N, so page N's items are shown twice until page N+1's data lands. React logs duplicate-key warnings in dev.
Pathname and params do not have this problem: they change together with the destination tree.
Must be fixed before the next release: #1020 documents clearOnReload with exactly this load-more pattern, and its fixtures and skill example filter duplicates to cope.
Repro
const Carried = createLocationState<{ items: Item[] }>({ clearOnReload: true });
function ProductList() {
const carried = useLocationState(Carried); // pages 1..N-1
const { items, page } = useLoader(ProductsLoader); // page N, from ?page=N
const all = [...(carried?.items ?? []), ...items];
return (
<>
<ul>{all.map((item) => <li key={item.id}>{item.name}</li>)}</ul>
<Link to={`?page=${page + 1}`} state={Carried({ items: all })}>
Load more
</Link>
</>
);
}Click "Load more" on page 5 with a loader that takes a moment.
Expected vs actual
While the navigation to ?page=6 is pending |
carried |
loader data | On screen |
|---|---|---|---|
| Expected | pages 1..4 (the current entry's) | page 5 | pages 1..5 |
| Actual | pages 1..5 (the destination's) | page 5 | pages 1..5 plus page 5 again |
After the loader lands both agree (pages 1..5 carried, page 6 loaded).
Where
Traced by reading main (eed288b1); the duplication was observed while writing the #1020 e2e fixtures.
packages/rangojs-router/src/browser/navigation-transaction.ts:212:tx.commitpushes the destination's history state and dispatches__rsc_locationstatesynchronously.packages/rangojs-router/src/browser/react/location-state.ts:134-144: the hook subscribes topopstateand__rsc_locationstatethroughuseSyncExternalStoreand re-readshistory.state, a synchronous update outside the navigation's transition.packages/rangojs-router/src/browser/partial-update.ts: the destination tree is rendered and committed aftertx.commit, inside the transition, and a navigation that waits on a loader keeps the old tree on screen until then.packages/rangojs-router/src/browser/react/use-pathname.ts: for contrast, pathname is read from the event controller through context and changes with the payload.- Other dispatch sites with the same shape:
src/browser/history-state.ts:198(mergeLocationState, server-set state),src/browser/navigation-bridge.ts:245,src/testing/render-route.tsx:800. Back/forward reaches the hook through its ownpopstatelistener.
To check as part of the fix
clientUrls()navigations, which render the destination component optimistically: whether the destination component sees its own entry's state on its first render, and whether a reader in the leaving tree or in a shared layout sees it too early.- Same-route navigations that hold the current content, intercepts, server-set state (
setLocationState, redirect state),flash,clearOnReload, and back/forward.
Direction
Deliver location state with the destination tree, the way route state is delivered, so a reader sees an entry's state only together with that entry's tree. Then remove the duplicate-filter workaround from the load-more example in skills/hooks/state.md and from the #1020 fixtures.
Activity
fix(router): location state belongs to the entry on screen; evicted s…