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 1/2] 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]); + }); }); From ac8c28931c987e6478b8782f4621be8943af485a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 02:38:22 +0000 Subject: [PATCH 2/2] fix(history): stamp unrouted entries from the last depth too (#694) saveCurrentDepth() stamped an entry the router didn't push (e.g. a #anchor jump) from history.length, which stops growing at the browser's cap. It now counts up from the last known depth, falling back to history.length only when none is known. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWNQHxQ9311k7tQMEKs --- .changeset/history-depth-capped-length.md | 2 +- src/routers/history.ts | 10 +++++++--- test/browser-history.spec.ts | 12 ++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.changeset/history-depth-capped-length.md b/.changeset/history-depth-capped-length.md index 5e033db3..c3846b05 100644 --- a/.changeset/history-depth-capped-length.md +++ b/.changeset/history-depth-capped-length.md @@ -2,4 +2,4 @@ "@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. +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. Entries the router did not push, such as a `#anchor` jump, are stamped one above the last known depth as well. diff --git a/src/routers/history.ts b/src/routers/history.ts index b70d4c0e..082ac748 100644 --- a/src/routers/history.ts +++ b/src/routers/history.ts @@ -14,10 +14,14 @@ export function bindEvent(target: EventTarget, type: string, handler: EventListe let depth: number; export function saveCurrentDepth() { const h = window.history; - if (!h.state || h.state._depth == null) { - h.replaceState({ ...h.state, _depth: h.length - 1 }, ""); + let d = h.state?._depth; + if (d == null) { + // an entry the router didn't push (e.g. a `#anchor` jump) sits one above + // the last known one; `length` only counts once nothing is known (#694) + d = depth == null ? h.length - 1 : depth + 1; + h.replaceState({ ...h.state, _depth: d }, ""); } - depth = h.state?._depth ?? h.length - 1; + depth = h.state?._depth ?? d; } function keepDepth(state: any) { diff --git a/test/browser-history.spec.ts b/test/browser-history.spec.ts index a1e4aaed..d9fa561a 100644 --- a/test/browser-history.spec.ts +++ b/test/browser-history.spec.ts @@ -29,6 +29,18 @@ describe("browserHistory", () => { expect(window.history.state._depth).toBe(51); }); + test("an entry pushed outside the router counts up from the last depth when history.length is capped", () => { + window.history.replaceState({ _depth: 49 }, "", "/"); + vi.spyOn(window.history, "length", "get").mockReturnValue(50); + const notify = vi.fn(); + const unbind = browserHistory().init!(notify); + window.history.pushState(null, "", "/#anchor"); + window.dispatchEvent(new PopStateEvent("popstate")); + unbind(); + expect(window.history.state._depth).toBe(50); + expect(notify).toHaveBeenCalledTimes(1); + }); + test("keeps user state when stamping the depth on push", () => { window.history.replaceState({ _depth: 3 }, "", "/"); const history = browserHistory();