Skip to content

location.state.__TSR_key is undefined on new navigation #6469

@kosei28

Description

@kosei28

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

  1. Enable scroll restoration using a custom getScrollRestorationKey:
    const router = createRouter({
      routeTree,
      scrollRestoration: true,
      getScrollRestorationKey: (location) => location.state.__TSR_key!,
    })
  2. Navigate to any page and scroll down.
  3. Click a <Link> to navigate to a new page.
  4. 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.

  1. precomputedLocation is created before history.push/replace is called:

    const precomputedLocation: ParsedLocation = {
      ...next,
      state: nextHistory.state,  // __TSR_key is not yet assigned at this point
      // ...
    }
  2. Then, history.push/replace is executed:

    const result = await this.history[next.replace ? 'replace' : 'push'](
      nextHistory.publicHref,
      nextHistory.state,
      // ...
    )
  3. Inside history.push, assignKeyAndIndex creates 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
      // ...
    }
  4. precomputedLocation is assigned to router.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.href as the key, and then returning to the original page, the system attempts to restore the position using location.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.

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