Skip to content

useLocationState shows the destination entry's state before the destination tree commits (load-more duplicates items while a navigation is pending) #1029

Closed

Description

@ivogt

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.commit pushes the destination's history state and dispatches __rsc_locationstate synchronously.
  • packages/rangojs-router/src/browser/react/location-state.ts:134-144: the hook subscribes to popstate and __rsc_locationstate through useSyncExternalStore and re-reads history.state, a synchronous update outside the navigation's transition.
  • packages/rangojs-router/src/browser/partial-update.ts: the destination tree is rendered and committed after tx.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 own popstate listener.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions