TanStack / TanStack/router

resetScroll from a navigation is lost when the destination navigates while it is still settling

Open
#8,028 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

Description

Describe the bug

A navigation's resetScroll intent is stored in a single slot on the router rather than per navigation:

// packages/router-core/src/router.ts
this._scroll.next = next.resetScroll ?? true

If a second navigation is committed while the first is still settling, it overwrites that slot and the first navigation's scroll reset is silently dropped.

This shows up with an ordinary pattern: a page derives state from its own data and writes it back into its URL on first render.

useEffect(() => {
  if (color) return
  navigate({
    to: '/results',
    replace: true,
    resetScroll: false, // correct: this is not a page change
    search: (prev) => ({ ...prev, color: 'w,u,c' }),
  })
}, [color, navigate])

resetScroll: false is correct for that navigation — it only adds a search param to the route you are already on, and it should not move the viewport. But it lands in the same slot as the resetScroll: true recorded by the link navigation that is still settling. That navigation then never resets, and the user is left looking at the new page at the previous page's scroll offset.

Omitting resetScroll: false is not a workaround either: the reset then happens, but under the second navigation's timing, so the destination is painted at the old scroll offset first and visibly jumps to the top afterwards.

The two navigations have independent, explicitly documented resetScroll values, so neither should be able to consume or cancel the other's.

Your Example Website or App

https://github.com/daveycodez/tanstack-router-scroll-repro

Steps to Reproduce the Bug or Issue
  1. npm install && npm run dev
  2. Scroll to the bottom of the home page
  3. Click "open the results page"

Or run the included Playwright test: npx playwright install chromium && npm test

Expected behavior

Following the link resets scroll to the top of the results page.

Actual behavior

Scroll stays at the home page's offset (~8400px). The read-out in the bottom-right corner turns red.

The only variable is resetScroll: false on the second navigation, over 3 runs each:

second navigation link navigation's reset
resetScroll: false never applied — stays at 8420
default (true) applied — 0
Platform
  • @tanstack/react-router: 1.170.25
  • Browser: Chromium 141
  • OS: macOS
Additional context

Found while debugging a search page that infers filters from its results and writes them into the URL after render — the write cannot happen in beforeLoad/SSR there, because the filtered URL must not be the indexed one.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/router-core/src/router.ts at the single-slot assignment to _scroll.next. Reproduce the race with the included Playwright test using npx playwright install chromium && npm test, then verify that the link navigation still resets scroll when the destination performs its own resetScroll: false navigation.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, react, typescript
Domain
frontend, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.