-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Which project does this relate to?
Router
Describe the bug
When performing a programmatic navigation (via navigate() or <Link>), location.state.__TSR_key is undefined, even though window.history.state.__TSR_key is correctly set.
As a result, using getScrollRestorationKey: (location) => location.state.__TSR_key! does not work as expected.
Note that location.state.__TSR_key is correctly set during the initial page load or when navigating using the browser's "Back/Forward" buttons.
| Scenario | location.state.__TSR_key |
history.state.__TSR_key |
|---|---|---|
| Initial Load | ✅ Set | ✅ Set |
| Browser Back/Forward | ✅ Set | ✅ Set |
| New Navigation (Link/navigate) | ❌ undefined |
✅ Set |
Your Example Website or App
N/A
Steps to Reproduce the Bug or Issue
- Enable scroll restoration using a custom
getScrollRestorationKey:const router = createRouter({ routeTree, scrollRestoration: true, getScrollRestorationKey: (location) => location.state.__TSR_key!, })
- Navigate to any page and scroll down.
- Click a
<Link>to navigate to a new page. - Go "Back" to the original page — the scroll position is not restored.
Expected behavior
After navigation, location.state.__TSR_key should always match window.history.state.__TSR_key.
Screenshots or Videos
No response
Platform
- Router / Start Version: 1.132.0
- OS: macOS
- Browser: Firefox
- Browser Version: 146.0.1
- Bundler: vite
- Bundler Version: 7.1.7
Additional context
Root Cause Analysis
The issue lies within commitLocation in router.ts.
-
precomputedLocationis created beforehistory.push/replaceis called:const precomputedLocation: ParsedLocation = { ...next, state: nextHistory.state, // __TSR_key is not yet assigned at this point // ... }
-
Then,
history.push/replaceis executed:const result = await this.history[next.replace ? 'replace' : 'push']( nextHistory.publicHref, nextHistory.state, // ... )
-
Inside
history.push,assignKeyAndIndexcreates and returns a new object containing__TSR_key:// history/src/index.ts push: (path, state, navigateOpts) => { state = assignKeyAndIndex(currentIndex + 1, state) // Returns a "new" object with __TSR_key // ... }
-
precomputedLocationis assigned torouter.latestLocation, but it still holds the old state object which lacks__TSR_key:this.latestLocation = precomputedLocation // state.__TSR_key remains undefined
Why initial loads and browser navigations work correctly:
This is because parseLocation() reads directly from window.history.state, where __TSR_key has already been set.
Workaround
You can work around this by using history.state as a fallback within getScrollRestorationKey:
const router = createRouter({
routeTree,
scrollRestoration: true,
getScrollRestorationKey: (location) =>
location.state.__TSR_key ?? window.history.state.__TSR_key,
})Behavior of the Default getScrollRestorationKey
The default implementation is as follows:
export const defaultGetScrollRestorationKey = (location: ParsedLocation) => {
return location.state.__TSR_key! || location.href
}This fallback to location.href appears inappropriate because it causes the following issues:
-
Scroll position is not restored when returning to a previous page: When moving to another page after the scroll position was saved using
location.hrefas the key, and then returning to the original page, the system attempts to restore the position usinglocation.state.__TSR_key. Since the keys do not match, restoration fails. -
Scroll position is restored when navigating to the same page via a different page: Since new navigations always fall back to
location.href, navigating to the same page without using the browser's "Back" button restores the previous scroll position. This is undesirable as restoration is typically only expected when returning to a page.
Key Fallback during Scroll Restoration
While restoreScroll (which restores the scroll position) implements a fallback to window.history.state.__TSR_key, the logic for saving the scroll position does not. This discrepancy prevents the expected behavior. If this bug is fixed, the fallback in restoreScroll will become unnecessary.