TanStack Router scrollRestoration Deep Dive - From Source Code to Implementation

In modern Single Page Application (SPA) development, user experience optimization often lies in the attention to detail. Among these, scroll position restoration is a feature that appears simple but is highly technical. This article will thoroughly analyze the implementation principles of TanStack Router’s scrollRestoration feature, from basic concepts to source code analysis, providing a comprehensive understanding of this important functionality.

Table of Contents

What is scrollRestoration?

scrollRestoration is a configuration option provided by TanStack Router that controls the behavior of scroll position restoration. When set to true, it will:

  • Automatically save scroll position: Save the current scroll position when the user leaves a page
  • Automatically restore scroll position: Restore to the previous scroll position when the user returns to a page
  • Enhance user experience: Avoid users having to re-scroll to their previous browsing position after navigation

Use Case Examples

E-commerce Website Scenario

Consider a user browsing a product listing page:

Without scrollRestoration:

  1. User scrolls to the 50th product position
  2. Clicks on a product to enter the detail page
  3. Clicks browser back button to return to listing page
  4. Page returns to the top, user needs to scroll again to find the 50th product 😫

With scrollRestoration enabled:

  1. User scrolls to the 50th product position
  2. Clicks on a product to enter the detail page
  3. Clicks browser back button to return to listing page
  4. Page automatically scrolls to the 50th product position 😊

Other Application Scenarios

  • Social Media Feed: Scroll to the 100th post, click on comments and return, still at the 100th post position
  • Document Reading: In the middle of a long document, click a link and return, still at the original reading position
  • Search Results: View an item on page 3 of search results, return and still on page 3

Basic Configuration and Usage

1
2
3
4
5
6
7
8
9
10
11
12
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
const router = createRouter({
routeTree,
scrollRestoration: true, // Enable scroll position restoration
})

return router
}

Technical Implementation Principles

1. Browser History API Foundation

Modern browsers provide native scroll restoration control:

1
2
3
// Browser native scrollRestoration
history.scrollRestoration = 'auto' // Browser automatic management
history.scrollRestoration = 'manual' // Manual management

TanStack Router takes control of scroll restoration by setting history.scrollRestoration = 'manual'.

2. Scroll Position Saving Mechanism

When routes change, TanStack Router saves the current scroll position:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Conceptual implementation of scroll position saving
const saveScrollPosition = () => {
const scrollData = {
x: window.scrollX,
y: window.scrollY,
timestamp: Date.now()
}

// Save to sessionStorage
const restoreKey = getKey(router.state.location)
scrollRestorationCache.set(restoreKey, {
'window': scrollData
})
}

3. Scroll Position Restoration Mechanism

When returning to a page, restore the scroll position:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Conceptual implementation of scroll position restoration
const restoreScrollPosition = (routeState) => {
if (routeState.scrollPosition) {
// Wait for page rendering to complete
requestAnimationFrame(() => {
window.scrollTo({
top: routeState.scrollPosition.y,
left: routeState.scrollPosition.x,
behavior: 'auto' // or 'smooth'
})
})
}
}

4. Critical Timing Control

Save Timing:

  • Before user clicks links to leave the page
  • Before browser forward/back navigation
  • Before route change triggers

Restore Timing:

  • After page components are mounted
  • After DOM rendering is complete
  • Using requestAnimationFrame to ensure layout completion

Source Code Deep Analysis

Based on in-depth analysis of TanStack Router’s official source code, here is the specific implementation of the scrollRestoration feature:

1. createRouter Function Entry

File Location: packages/react-router/src/router.ts:80

1
2
3
export const createRouter: CreateRouterFn = (options) => {
return new Router(options)
}

2. scrollRestoration Option Definition

File Location: packages/router-core/src/router.ts:390-414

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export interface RouterOptions<...> {
/**
* If `true`, scroll restoration will be enabled
* @default false
*/
scrollRestoration?: boolean

/**
* A function that will be called to get the key for the scroll restoration cache.
* @default (location) => location.href
*/
getScrollRestorationKey?: (location: ParsedLocation) => string

/**
* The default behavior for scroll restoration.
* @default 'auto'
*/
scrollRestorationBehavior?: ScrollBehavior

/**
* An array of selectors that will be used to scroll to the top of the page in addition to `window`
* @default ['window']
*/
scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>
}

3. Scroll Restoration Setup in Router Initialization

File Location: packages/router-core/src/router.ts:924

1
2
3
4
5
6
7
8
9
10
// In Router's update method
if (!this.__store) {
this.__store = new Store(getInitialRouterState(this.latestLocation), {
onUpdate: () => {
// ...
},
})

setupScrollRestoration(this) // Key: Initialize scroll restoration here
}

4. setupScrollRestoration Core Implementation

File Location: packages/router-core/src/scroll-restoration.ts:209-353

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
if (scrollRestorationCache === undefined) {
return
}

const shouldScrollRestoration = force ?? router.options.scrollRestoration ?? false

if (shouldScrollRestoration) {
router.isScrollRestoring = true
}

if (typeof document === 'undefined' || router.isScrollRestorationSetup) {
return
}

router.isScrollRestorationSetup = true

const getKey = router.options.getScrollRestorationKey || defaultGetScrollRestorationKey

// Key: Set browser scroll restoration to manual mode
window.history.scrollRestoration = 'manual'

// Listen to scroll events and cache scroll positions
const onScroll = (event: Event) => {
if (ignoreScroll || !router.isScrollRestoring) {
return
}

let elementSelector = ''

if (event.target === document || event.target === window) {
elementSelector = 'window'
} else {
const attrId = (event.target as Element).getAttribute('data-scroll-restoration-id')

if (attrId) {
elementSelector = `[data-scroll-restoration-id="${attrId}"]`
} else {
elementSelector = getCssSelector(event.target)
}
}

const restoreKey = getKey(router.state.location)

// Save scroll position to sessionStorage
scrollRestorationCache.set((state) => {
const keyEntry = (state[restoreKey] = state[restoreKey] || {})
const elementEntry = (keyEntry[elementSelector] = keyEntry[elementSelector] || {})

if (elementSelector === 'window') {
elementEntry.scrollX = window.scrollX || 0
elementEntry.scrollY = window.scrollY || 0
} else if (elementSelector) {
const element = document.querySelector(elementSelector)
if (element) {
elementEntry.scrollX = element.scrollLeft || 0
elementEntry.scrollY = element.scrollTop || 0
}
}

return state
})
}

// Throttle scroll events, trigger at most once every 100ms
if (typeof document !== 'undefined') {
document.addEventListener('scroll', throttle(onScroll, 100), true)
}

// Listen to route rendering completion event, restore scroll position
router.subscribe('onRendered', (event) => {
const cacheKey = getKey(event.toLocation)

if (!router.resetNextScroll) {
router.resetNextScroll = true
return
}

restoreScroll({
storageKey,
key: cacheKey,
behavior: router.options.scrollRestorationBehavior,
shouldScrollRestoration: router.isScrollRestoring,
scrollToTopSelectors: router.options.scrollToTopSelectors,
location: router.history.location,
})
})
}

5. restoreScroll Implementation

File Location: packages/router-core/src/scroll-restoration.ts:104-207

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
export function restoreScroll({
storageKey,
key,
behavior,
shouldScrollRestoration,
scrollToTopSelectors,
location,
}: {
storageKey: string
key?: string
behavior?: ScrollToOptions['behavior']
shouldScrollRestoration?: boolean
scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>
location?: HistoryLocation
}) {
let byKey: ScrollRestorationByKey

try {
byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')
} catch (error: any) {
console.error(error)
return
}

const resolvedKey = key || window.history.state?.key
const elementEntries = byKey[resolvedKey]

ignoreScroll = true

// Priority restore cached scroll position
if (shouldScrollRestoration && elementEntries && Object.keys(elementEntries).length > 0) {
for (const elementSelector in elementEntries) {
const entry = elementEntries[elementSelector]!
if (elementSelector === 'window') {
window.scrollTo({
top: entry.scrollY,
left: entry.scrollX,
behavior,
})
} else if (elementSelector) {
const element = document.querySelector(elementSelector)
if (element) {
element.scrollLeft = entry.scrollX
element.scrollTop = entry.scrollY
}
}
}
return
}

// Handle hash scrolling
const hash = (location ?? window.location).hash.split('#')[1]

if (hash) {
const hashScrollIntoViewOptions = (window.history.state || {}).__hashScrollIntoViewOptions ?? true

if (hashScrollIntoViewOptions) {
const el = document.getElementById(hash)
if (el) {
el.scrollIntoView(hashScrollIntoViewOptions)
}
}
return
}

// Default scroll to top of page
['window', ...(scrollToTopSelectors?.filter((d) => d !== 'window') ?? [])].forEach((selector) => {
const element = selector === 'window' ? window : typeof selector === 'function' ? selector() : document.querySelector(selector)
if (element) {
element.scrollTo({
top: 0,
left: 0,
behavior,
})
}
})

ignoreScroll = false
}

6. Cache System Implementation

File Location: packages/router-core/src/scroll-restoration.ts:36-73

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Storage key definition
export const storageKey = 'tsr-scroll-restoration-v1_3'

// Throttle function implementation
const throttle = (fn: (...args: Array<any>) => void, wait: number) => {
let timeout: any
return (...args: Array<any>) => {
if (!timeout) {
timeout = setTimeout(() => {
fn(...args)
timeout = null
}, wait)
}
}
}

// Create scroll restoration cache
function createScrollRestorationCache(): ScrollRestorationCache | undefined {
const safeSessionStorage = getSafeSessionStorage()
if (!safeSessionStorage) {
return undefined
}

const persistedState = safeSessionStorage.getItem(storageKey)
let state: ScrollRestorationByKey = persistedState ? JSON.parse(persistedState) : {}

return {
state,
set: (updater) => (
(state = functionalUpdate(updater, state) || state),
safeSessionStorage.setItem(storageKey, JSON.stringify(state))
),
}
}

export const scrollRestorationCache = createScrollRestorationCache()

7. Data Storage Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Data structure in sessionStorage
{
"tsr-scroll-restoration-v1_3": {
"/products": { // Route path as key
"window": { // Element selector
"scrollX": 0,
"scrollY": 1250
}
},
"/products/123": {
"window": {
"scrollX": 0,
"scrollY": 800
}
}
}
}

Scroll Restoration Workflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sequenceDiagram
participant User as User
participant Router as TanStack Router
participant Browser as Browser
participant Storage as SessionStorage

Note over Router: Initialization Phase
Router->>Browser: history.scrollRestoration = 'manual'
Router->>Browser: addEventListener('scroll', onScroll)
Router->>Router: setupScrollRestoration()

Note over User: User Browsing and Navigation
User->>Browser: Scroll page
Browser->>Router: scroll event
Router->>Storage: Save scroll position { x, y }

User->>Router: Click link navigation
Router->>Storage: Save current page scroll position
Router->>Browser: Navigate to new page

Note over User: User Return Operation
User->>Browser: Click back button
Browser->>Router: Route change event
Router->>Storage: Read saved scroll position
Router->>Router: Wait for page rendering completion
Router->>Browser: window.scrollTo(x, y)
Browser->>User: Page scrolls to previous position

Advanced Features

1. Multi-element Scroll Support

TanStack Router supports not only window-level scrolling, but also scrolling containers within the page:

1
2
3
4
5
6
7
8
9
// Support multiple scroll elements
const elementSelector = event.target === window
? 'window'
: getCssSelector(event.target)

// Set scroll restoration ID for specific elements
<div data-scroll-restoration-id="product-list" className="overflow-auto">
{/* Scrolling content */}
</div>

2. Custom Scroll Restoration Key

1
2
3
4
5
6
7
8
const router = createRouter({
routeTree,
scrollRestoration: true,
getScrollRestorationKey: (location) => {
// Custom cache key generation strategy
return `${location.pathname}${location.search}`
}
})

3. Scroll Behavior Configuration

1
2
3
4
5
6
const router = createRouter({
routeTree,
scrollRestoration: true,
scrollRestorationBehavior: 'smooth', // 'auto' | 'smooth'
scrollToTopSelectors: ['window', '.main-content']
})

Performance Optimization Mechanisms

1. Event Throttling

1
2
// Use throttling function, save at most once every 100ms
document.addEventListener('scroll', throttle(onScroll, 100), true)

2. Smart Ignore Mechanism

1
2
3
4
5
6
7
8
9
let ignoreScroll = false // Prevent triggering save during scroll restoration

const restoreScroll = () => {
ignoreScroll = true
window.scrollTo(x, y)
setTimeout(() => {
ignoreScroll = false
}, 100)
}

3. Rendering Completion Detection

1
2
3
4
5
6
7
8
// Use requestAnimationFrame to ensure DOM rendering completion
requestAnimationFrame(() => {
window.scrollTo({
top: savedPosition.y,
left: savedPosition.x,
behavior: 'auto'
})
})

Edge Case Handling

1. Page Height Changes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Handle page height changes caused by dynamic content loading
const restoreWithRetry = (targetY) => {
let attempts = 0
const maxAttempts = 10

const tryRestore = () => {
const currentHeight = document.body.scrollHeight

if (currentHeight >= targetY || attempts >= maxAttempts) {
window.scrollTo(0, Math.min(targetY, currentHeight))
return
}

attempts++
setTimeout(tryRestore, 100) // Delayed retry
}

tryRestore()
}
1
2
3
4
5
6
7
8
9
// Priority handling of hash scrolling
const hash = window.location.hash.split('#')[1]
if (hash) {
const element = document.getElementById(hash)
if (element) {
element.scrollIntoView()
return
}
}

3. Error Handling

1
2
3
4
5
6
7
try {
const cachedData = JSON.parse(sessionStorage.getItem(storageKey) || '{}')
} catch (error) {
console.error('Failed to parse scroll restoration data:', error)
// Fallback to default behavior
window.scrollTo(0, 0)
}

Conclusion

TanStack Router’s scrollRestoration feature achieves precise scroll position restoration through the following key technologies:

  1. Browser API Takeover: Takes control of native browser behavior through history.scrollRestoration = 'manual'
  2. Event Listening Mechanism: Monitors scroll events and saves scroll positions in real-time
  3. SessionStorage Persistence: Uses sessionStorage to save scroll data across pages
  4. Smart Restoration Strategy: Combines requestAnimationFrame and retry mechanisms to ensure accurate restoration
  5. Performance Optimization: Optimizes performance through throttling, ignore mechanisms, etc.
  6. Multi-element Support: Supports not only window scrolling but also scrolling containers within pages

This feature appears simple, but its implementation involves deep understanding of browser APIs, event handling optimization, data persistence strategies, and multiple technical layers. Through source code analysis, we can see the TanStack Router team’s meticulous consideration and technical depth in user experience optimization.

For modern SPA applications, enabling scrollRestoration: true is a simple yet effective user experience enhancement solution. It not only has sophisticated technical implementation, but more importantly, it truly solves the pain point problem users encounter when navigating within applications.