Skip to content

Commit 776d8ef

Browse files
fix(router-core): skip scroll restoration setup when disabled (#7562)
1 parent 996b9be commit 776d8ef

5 files changed

Lines changed: 136 additions & 36 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/router-core': patch
3+
---
4+
5+
Prevent scroll restoration listeners from being installed when scroll restoration is disabled.

packages/router-core/src/router.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -964,13 +964,16 @@ export class RouterCore<
964964
tempLocationKey: string | undefined = `${Math.round(
965965
Math.random() * 10000000,
966966
)}`
967-
resetNextScroll = true
967+
_scroll: {
968+
next: boolean
969+
restoring?: boolean
970+
restoration?: boolean
971+
reset?: boolean
972+
} = { next: true }
968973
shouldViewTransition?: boolean | ViewTransitionOptions = undefined
969974
isViewTransitionTypesSupported?: boolean = undefined
970975
subscribers = new Set<RouterListener<RouterEvent>>()
971976
viewTransitionPromise?: ControlledPromise<true>
972-
isScrollRestoring = false
973-
isScrollRestorationSetup = false
974977

975978
// Must build in constructor
976979
stores!: RouterStores<TRouteTree>
@@ -2227,7 +2230,7 @@ export class RouterCore<
22272230
)
22282231
}
22292232

2230-
this.resetNextScroll = next.resetScroll ?? true
2233+
this._scroll.next = next.resetScroll ?? true
22312234

22322235
if (!this.history.subscribers.size) {
22332236
this.load(

packages/router-core/src/scroll-restoration.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,17 @@ function getScrollToTopElements(
167167

168168
export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
169169
// Keep hash/top scrolling active even when sessionStorage is unavailable.
170+
const shouldSetupScrollRestoration = force ?? router.options.scrollRestoration
171+
const scroll = router._scroll
170172

171-
if (force ?? router.options.scrollRestoration) {
172-
router.isScrollRestoring = true
173+
if (shouldSetupScrollRestoration) {
174+
scroll.restoring = true
173175
}
174176

175-
if ((isServer ?? router.isServer) || router.isScrollRestorationSetup) {
177+
if (isServer ?? router.isServer) {
176178
return
177179
}
178180

179-
router.isScrollRestorationSetup = true
180-
ignoreScroll = false
181-
182181
const getKey =
183182
router.options.getScrollRestorationKey || defaultGetScrollRestorationKey
184183
const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()
@@ -194,10 +193,8 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
194193
trackedScrollEntries.set(target, entry)
195194
}
196195

197-
history.scrollRestoration = 'manual'
198-
199196
const onScroll = (event: Event) => {
200-
if (ignoreScroll || !router.isScrollRestoring) {
197+
if (ignoreScroll || !scroll.restoring) {
201198
return
202199
}
203200

@@ -211,7 +208,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
211208

212209
// Snapshot the current page's tracked scroll targets before navigation or unload.
213210
const snapshotCurrentScrollTargets = (restoreKey: string) => {
214-
if (!router.isScrollRestoring) {
211+
if (!scroll.restoring) {
215212
return
216213
}
217214

@@ -227,32 +224,45 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
227224
}
228225
}
229226

230-
document.addEventListener('scroll', onScroll, true)
231-
router.subscribe('onBeforeLoad', (event) => {
232-
if (event.fromLocation) {
233-
snapshotCurrentScrollTargets(getKey(event.fromLocation))
234-
}
235-
trackedScrollEntries.clear()
236-
})
237-
addEventListener('pagehide', () => {
238-
snapshotCurrentScrollTargets(
239-
getKey(
240-
router.stores.resolvedLocation.get() ?? router.stores.location.get(),
241-
),
242-
)
243-
persistScrollRestorationCache()
244-
})
227+
if (shouldSetupScrollRestoration && !scroll.restoration) {
228+
scroll.restoration = true
229+
ignoreScroll = false
230+
231+
history.scrollRestoration = 'manual'
232+
233+
document.addEventListener('scroll', onScroll, true)
234+
router.subscribe('onBeforeLoad', (event) => {
235+
if (event.fromLocation) {
236+
snapshotCurrentScrollTargets(getKey(event.fromLocation))
237+
}
238+
trackedScrollEntries.clear()
239+
})
240+
addEventListener('pagehide', () => {
241+
snapshotCurrentScrollTargets(
242+
getKey(
243+
router.stores.resolvedLocation.get() ?? router.stores.location.get(),
244+
),
245+
)
246+
persistScrollRestorationCache()
247+
})
248+
}
249+
250+
if (scroll.reset) {
251+
return
252+
}
253+
254+
scroll.reset = true
245255

246256
// Restore destination scroll after the new route has rendered.
247257
router.subscribe('onRendered', (event) => {
248258
const behavior = router.options.scrollRestorationBehavior
249259
const scrollToTopSelectors = router.options.scrollToTopSelectors
250-
const shouldResetScroll = router.resetNextScroll
260+
const shouldResetScroll = scroll.next
251261
let scrollToTopElements: Array<Element> | undefined
252262
trackedScrollEntries.clear()
253263

254264
if (!shouldResetScroll) {
255-
router.resetNextScroll = true
265+
scroll.next = true
256266
}
257267

258268
if (
@@ -265,7 +275,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
265275
const cacheKey = getKey(event.toLocation)
266276
const fromCacheKey = event.fromLocation && getKey(event.fromLocation)
267277

268-
if (router.isScrollRestoring && fromCacheKey && fromCacheKey !== cacheKey) {
278+
if (scroll.restoring && fromCacheKey && fromCacheKey !== cacheKey) {
269279
const fromElementEntries = scrollRestorationCache[fromCacheKey]
270280

271281
if (fromElementEntries) {
@@ -317,7 +327,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
317327
hashScrollIntoViewOptions &&
318328
(action === 'PUSH' || action === 'REPLACE')
319329

320-
const elementEntries = router.isScrollRestoring
330+
const elementEntries = scroll.restoring
321331
? scrollRestorationCache[cacheKey]
322332
: undefined
323333

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { createMemoryHistory } from '@tanstack/history'
2+
import { afterEach, describe, expect, test, vi } from 'vitest'
3+
import { BaseRootRoute, BaseRoute } from '../src'
4+
import { createTestRouter } from './routerTestUtils'
5+
6+
function createRouter(options: { scrollRestoration?: boolean } = {}) {
7+
const rootRoute = new BaseRootRoute({})
8+
const indexRoute = new BaseRoute({
9+
getParentRoute: () => rootRoute,
10+
path: '/',
11+
})
12+
13+
return createTestRouter({
14+
routeTree: rootRoute.addChildren([indexRoute]),
15+
history: createMemoryHistory({ initialEntries: ['/'] }),
16+
...options,
17+
})
18+
}
19+
20+
afterEach(() => {
21+
vi.restoreAllMocks()
22+
})
23+
24+
describe('setupScrollRestoration', () => {
25+
test('sets up scroll restoration when scrollRestoration is true', () => {
26+
const windowAddEventListener = vi.spyOn(window, 'addEventListener')
27+
const documentAddEventListener = vi.spyOn(document, 'addEventListener')
28+
const previousScrollRestoration = window.history.scrollRestoration
29+
30+
window.history.scrollRestoration = 'auto'
31+
32+
const router = createRouter({ scrollRestoration: true })
33+
34+
expect(router._scroll.restoring).toBe(true)
35+
expect(router._scroll.restoration).toBe(true)
36+
expect(window.history.scrollRestoration).toBe('manual')
37+
expect(
38+
windowAddEventListener.mock.calls.some(([event]) => event === 'pagehide'),
39+
).toBe(true)
40+
expect(
41+
documentAddEventListener.mock.calls.some(
42+
([event, _listener, options]) => event === 'scroll' && options === true,
43+
),
44+
).toBe(true)
45+
46+
window.history.scrollRestoration = previousScrollRestoration
47+
})
48+
49+
test.each([
50+
['omitted', undefined],
51+
['false', false],
52+
] as const)(
53+
'does not setup scroll restoration when scrollRestoration is %s',
54+
(_name, scrollRestoration) => {
55+
const windowAddEventListener = vi.spyOn(window, 'addEventListener')
56+
const documentAddEventListener = vi.spyOn(document, 'addEventListener')
57+
const previousScrollRestoration = window.history.scrollRestoration
58+
59+
window.history.scrollRestoration = 'auto'
60+
61+
const router = createRouter(
62+
scrollRestoration === undefined ? {} : { scrollRestoration },
63+
)
64+
65+
expect(router._scroll.restoring).toBeUndefined()
66+
expect(router._scroll.restoration).toBeUndefined()
67+
expect(router._scroll.reset).toBe(true)
68+
expect(window.history.scrollRestoration).toBe('auto')
69+
expect(
70+
windowAddEventListener.mock.calls.some(
71+
([event]) => event === 'pagehide',
72+
),
73+
).toBe(false)
74+
expect(
75+
documentAddEventListener.mock.calls.some(
76+
([event, _listener, options]) =>
77+
event === 'scroll' && options === true,
78+
),
79+
).toBe(false)
80+
81+
window.history.scrollRestoration = previousScrollRestoration
82+
},
83+
)
84+
})

packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,11 +391,9 @@ export const BaseTanStackRouterDevtoolsPanel =
391391
![
392392
'stores',
393393
'basepath',
394-
'injectedHtml',
395394
'subscribers',
396395
'latestLoadPromise',
397-
'navigateTimeout',
398-
'resetNextScroll',
396+
'_scroll',
399397
'tempLocationKey',
400398
'latestLocation',
401399
'routeTree',

0 commit comments

Comments
 (0)