From 1865ccc0c7822db651d7cfb8ee1262f1a14249a9 Mon Sep 17 00:00:00 2001 From: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:39:24 +0530 Subject: [PATCH] fix(history): stamp pushed depth from the current entry, not history.length Browsers cap session history (50 entries in Chromium and Firefox). Once the cap is reached, pushState drops the oldest entry and history.length stays the same, so `history.length - 1` gave every new entry the depth of the one below it. Back then computed a zero delta (useBeforeLeave was skipped) and scroll restoration shared one slot between entries. browserHistory() and hashHistory() now stamp a pushed entry with the current depth + 1. Replace keeps the current depth as before. Fixes #694 --- .changeset/history-depth-capped-length.md | 5 +++ src/routers/history.ts | 15 +++++++-- test/browser-history.spec.ts | 40 ++++++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 .changeset/history-depth-capped-length.md diff --git a/.changeset/history-depth-capped-length.md b/.changeset/history-depth-capped-length.md new file mode 100644 index 00000000..5e033db3 --- /dev/null +++ b/.changeset/history-depth-capped-length.md @@ -0,0 +1,5 @@ +--- +"@solidjs/router": patch +--- + +Stamp a pushed history entry's `_depth` as one above the current entry instead of `history.length - 1`. Browsers cap session history (50 entries in Chromium and Firefox), so once the cap was reached every new entry got the same depth. That broke scroll restoration and `useBeforeLeave` back-navigation deltas on long sessions. diff --git a/src/routers/history.ts b/src/routers/history.ts index 18c55c3f..b70d4c0e 100644 --- a/src/routers/history.ts +++ b/src/routers/history.ts @@ -27,6 +27,17 @@ function keepDepth(state: any) { }; } +// A pushed entry sits one above the current one. Stamp it from `depth`, not +// `history.length`: once the browser's session history is full (50 entries in +// Chromium and Firefox) a push drops the oldest entry and `length` stays the +// same, so `length - 1` would give every new entry the depth of the one below. +function nextDepth(state: any) { + return { + ...state, + _depth: (depth ?? window.history.length - 1) + 1 + }; +} + function notifyIfNotBlocked( notify: (value?: string | LocationChange) => void, block: (delta: number | null) => boolean @@ -87,7 +98,7 @@ export function browserHistory(): RouterHistory { if (replace) { window.history.replaceState(keepDepth(state), "", value); } else { - window.history.pushState(state, "", value); + window.history.pushState(nextDepth(state), "", value); } scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll); saveCurrentDepth(); @@ -136,7 +147,7 @@ export function hashHistory(): RouterHistory { if (replace) { window.history.replaceState(keepDepth(state), "", "#" + value); } else { - window.history.pushState(state, "", "#" + value); + window.history.pushState(nextDepth(state), "", "#" + value); } const hashIndex = value.indexOf("#"); const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : ""; diff --git a/test/browser-history.spec.ts b/test/browser-history.spec.ts index c49e7987..a1e4aaed 100644 --- a/test/browser-history.spec.ts +++ b/test/browser-history.spec.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test, vi } from "vitest"; -import { browserHistory } from "../src/index.js"; +import { browserHistory, hashHistory } from "../src/index.js"; describe("browserHistory", () => { afterEach(() => vi.restoreAllMocks()); @@ -10,4 +10,42 @@ describe("browserHistory", () => { expect(() => browserHistory()).not.toThrow(); }); + + // #694: browsers cap session history (50 entries in Chromium and Firefox). + // Once full, a push drops the oldest entry and history.length stops growing. + test("stamps increasing depths on push when history.length is capped", () => { + window.history.replaceState({ _depth: 48 }, "", "/"); + vi.spyOn(window.history, "length", "get").mockReturnValue(50); + const history = browserHistory(); + + const depths: number[] = []; + for (let i = 1; i <= 3; i++) { + history.set({ value: `/${i}` }); + depths.push(window.history.state._depth); + } + expect(depths).toEqual([49, 50, 51]); + + history.set({ value: "/3?replaced", replace: true }); + expect(window.history.state._depth).toBe(51); + }); + + test("keeps user state when stamping the depth on push", () => { + window.history.replaceState({ _depth: 3 }, "", "/"); + const history = browserHistory(); + history.set({ value: "/next", state: { from: "test" } }); + expect(window.history.state).toEqual({ from: "test", _depth: 4 }); + }); + + test("hashHistory stamps increasing depths on push when history.length is capped", () => { + window.history.replaceState({ _depth: 48 }, "", "/"); + vi.spyOn(window.history, "length", "get").mockReturnValue(50); + const history = hashHistory(); + + const depths: number[] = []; + for (let i = 1; i <= 3; i++) { + history.set({ value: `/${i}` }); + depths.push(window.history.state._depth); + } + expect(depths).toEqual([49, 50, 51]); + }); });