diff --git a/e2e/react-router/basepath-file-based/src/routeTree.gen.ts b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts
index 177ee7a3136..2989a394d3a 100644
--- a/e2e/react-router/basepath-file-based/src/routeTree.gen.ts
+++ b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts
@@ -9,11 +9,17 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
+import { Route as ScrollErrorRouteImport } from './routes/scroll-error'
import { Route as RedirectReloadRouteImport } from './routes/redirectReload'
import { Route as RedirectRouteImport } from './routes/redirect'
import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
+const ScrollErrorRoute = ScrollErrorRouteImport.update({
+ id: '/scroll-error',
+ path: '/scroll-error',
+ getParentRoute: () => rootRouteImport,
+} as any)
const RedirectReloadRoute = RedirectReloadRouteImport.update({
id: '/redirectReload',
path: '/redirectReload',
@@ -40,12 +46,14 @@ export interface FileRoutesByFullPath {
'/about': typeof AboutRoute
'/redirect': typeof RedirectRoute
'/redirectReload': typeof RedirectReloadRoute
+ '/scroll-error': typeof ScrollErrorRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/redirect': typeof RedirectRoute
'/redirectReload': typeof RedirectReloadRoute
+ '/scroll-error': typeof ScrollErrorRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -53,13 +61,20 @@ export interface FileRoutesById {
'/about': typeof AboutRoute
'/redirect': typeof RedirectRoute
'/redirectReload': typeof RedirectReloadRoute
+ '/scroll-error': typeof ScrollErrorRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/' | '/about' | '/redirect' | '/redirectReload'
+ fullPaths: '/' | '/about' | '/redirect' | '/redirectReload' | '/scroll-error'
fileRoutesByTo: FileRoutesByTo
- to: '/' | '/about' | '/redirect' | '/redirectReload'
- id: '__root__' | '/' | '/about' | '/redirect' | '/redirectReload'
+ to: '/' | '/about' | '/redirect' | '/redirectReload' | '/scroll-error'
+ id:
+ | '__root__'
+ | '/'
+ | '/about'
+ | '/redirect'
+ | '/redirectReload'
+ | '/scroll-error'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -67,10 +82,18 @@ export interface RootRouteChildren {
AboutRoute: typeof AboutRoute
RedirectRoute: typeof RedirectRoute
RedirectReloadRoute: typeof RedirectReloadRoute
+ ScrollErrorRoute: typeof ScrollErrorRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
+ '/scroll-error': {
+ id: '/scroll-error'
+ path: '/scroll-error'
+ fullPath: '/scroll-error'
+ preLoaderRoute: typeof ScrollErrorRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/redirectReload': {
id: '/redirectReload'
path: '/redirectReload'
@@ -107,6 +130,7 @@ const rootRouteChildren: RootRouteChildren = {
AboutRoute: AboutRoute,
RedirectRoute: RedirectRoute,
RedirectReloadRoute: RedirectReloadRoute,
+ ScrollErrorRoute: ScrollErrorRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/e2e/react-router/basepath-file-based/src/routes/scroll-error.tsx b/e2e/react-router/basepath-file-based/src/routes/scroll-error.tsx
new file mode 100644
index 00000000000..227c152172e
--- /dev/null
+++ b/e2e/react-router/basepath-file-based/src/routes/scroll-error.tsx
@@ -0,0 +1,16 @@
+import { Link, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/scroll-error')({
+ component: ScrollErrorComponent,
+})
+
+function ScrollErrorComponent() {
+ return (
+
+
About
+
+
Scroll Error Test
+
+
+ )
+}
diff --git a/e2e/react-router/basepath-file-based/tests/scroll-restoration-session-storage-error.test.ts b/e2e/react-router/basepath-file-based/tests/scroll-restoration-session-storage-error.test.ts
new file mode 100644
index 00000000000..ab0bb4bc993
--- /dev/null
+++ b/e2e/react-router/basepath-file-based/tests/scroll-restoration-session-storage-error.test.ts
@@ -0,0 +1,101 @@
+/* eslint-disable */
+import { expect, test } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+const trackConsole = (page: Page) => {
+ const consoleWarnings: Array = []
+
+ page.on('console', (msg) => {
+ if (msg.type() === 'warning') {
+ consoleWarnings.push(msg.text())
+ }
+ })
+
+ return consoleWarnings
+}
+
+test.describe('Scroll Restoration with Session Storage Error', () => {
+ test('should not crash when sessionStorage.setItem throws an error', async ({
+ page,
+ }) => {
+ const consoleWarnings = trackConsole(page)
+
+ await page.goto('/app/scroll-error')
+ await page.waitForLoadState('networkidle')
+
+ await page.evaluate(() => {
+ sessionStorage.setItem = () => {
+ throw new Error('Test Error')
+ }
+ })
+
+ await page.evaluate(() => window.scrollTo(0, 200))
+ await page.waitForTimeout(150)
+
+ await page.click('a[href="/app/about"]')
+ await page.waitForLoadState('networkidle')
+
+ await page.goBack()
+ await page.waitForLoadState('networkidle')
+
+ expect(
+ consoleWarnings.some((warning) =>
+ warning.includes(
+ '[ts-router] Could not persist scroll restoration state to sessionStorage.',
+ ),
+ ),
+ ).toBeTruthy()
+
+ const heading = page.locator('h1:has-text("Scroll Error Test")')
+ await expect(heading).toBeVisible()
+
+ const scrollPosition = await page.evaluate(() => window.scrollY)
+ expect(scrollPosition).not.toBe(200)
+ })
+
+ test('should surface warning when sessionStorage quota is exceeded', async ({
+ page,
+ }) => {
+ const consoleWarnings = trackConsole(page)
+
+ await page.goto('/app/scroll-error')
+ await page.waitForLoadState('networkidle')
+
+ await page.evaluate(() => {
+ let i = 0
+ const chunk = 'x'.repeat(32)
+
+ try {
+ while (true) {
+ sessionStorage.setItem(`key_${i}`, chunk)
+ i += 1
+ }
+ } catch {
+ console.log(`Stored ${i} keys in session storage`)
+ }
+ })
+
+ await page.evaluate(() => window.scrollTo(0, 200))
+ await page.waitForTimeout(150)
+
+ await page.click('a[href="/app/about"]')
+ await page.waitForLoadState('networkidle')
+
+ await page.goBack()
+ await page.waitForLoadState('networkidle')
+
+ expect(
+ consoleWarnings.some((warning) =>
+ warning.includes(
+ '[ts-router] Could not persist scroll restoration state to sessionStorage.',
+ ),
+ ),
+ ).toBeTruthy()
+
+ const heading = page.locator('h1:has-text("Scroll Error Test")')
+ await expect(heading).toBeVisible()
+
+ const scrollPosition = await page.evaluate(() => window.scrollY)
+ expect(scrollPosition).not.toBe(200)
+ })
+})
diff --git a/packages/router-core/src/scroll-restoration.ts b/packages/router-core/src/scroll-restoration.ts
index 22d5ecb558d..c8614429a9b 100644
--- a/packages/router-core/src/scroll-restoration.ts
+++ b/packages/router-core/src/scroll-restoration.ts
@@ -66,10 +66,16 @@ function createScrollRestorationCache(): ScrollRestorationCache | null {
// This setter is simply to make sure that we set the sessionStorage right
// after the state is updated. It doesn't necessarily need to be a functional
// update.
- set: (updater) => (
- (state = functionalUpdate(updater, state) || state),
- safeSessionStorage.setItem(storageKey, JSON.stringify(state))
- ),
+ set: (updater) => {
+ state = functionalUpdate(updater, state) || state
+ try {
+ safeSessionStorage.setItem(storageKey, JSON.stringify(state))
+ } catch {
+ console.warn(
+ '[ts-router] Could not persist scroll restoration state to sessionStorage.',
+ )
+ }
+ },
}
}