|
3 | 3 | import { useEffect } from 'react' |
4 | 4 | import { usePathname } from 'next/navigation' |
5 | 5 |
|
6 | | -/** |
7 | | - * Timestamp of the most recent popstate. Module-scoped so it survives layout |
8 | | - * remounts when navigating across sections (e.g., blog ↔ integrations ↔ models), |
9 | | - * where the outgoing layout's component instances unmount before the incoming |
10 | | - * layout's effect runs. |
11 | | - * |
12 | | - * Initialized to `-Infinity` so initial mounts don't mimic a recent popstate. |
13 | | - * Consumed on first use (reset back to `-Infinity`) so a real link navigation |
14 | | - * immediately after Back isn't swallowed. The timestamp window provides a |
15 | | - * safety net for popstates that never trigger a pathname effect (e.g., |
16 | | - * hash-only back/forward), letting the signal self-expire. |
17 | | - */ |
18 | | -let lastPopstateAt = Number.NEGATIVE_INFINITY |
19 | | -const POPSTATE_WINDOW_MS = 200 |
20 | | - |
21 | | -/** |
22 | | - * Captured at module evaluation time. When this module is bundled into the |
23 | | - * initial page payload (direct load / reload of a shelled route), readyState |
24 | | - * is still `loading` or `interactive`, the browser will restore scroll on |
25 | | - * reload, and we should skip the first reset. When the module is dynamically |
26 | | - * imported during a client-side navigation (e.g., user clicked from `/` into |
27 | | - * `/blog/x`), readyState is already `complete` and the first mount is a real |
28 | | - * route change that should scroll to top. |
29 | | - */ |
30 | | -const wasInitialPageLoad = typeof document !== 'undefined' && document.readyState !== 'complete' |
31 | | - |
32 | | -/** |
33 | | - * Tracks whether any `ScrollToTop` instance has run its mount effect yet. |
34 | | - * Module-scoped so cross-section navigation (which mounts a fresh instance) |
35 | | - * doesn't re-trigger the initial-mount guard. |
36 | | - */ |
37 | | -let hasMounted = false |
38 | | - |
39 | | -if (typeof window !== 'undefined') { |
40 | | - window.addEventListener('popstate', () => { |
41 | | - lastPopstateAt = performance.now() |
42 | | - }) |
43 | | -} |
44 | | - |
45 | 6 | /** |
46 | 7 | * Resets window scroll to the top on App Router pathname changes. |
47 | 8 | * |
48 | 9 | * Next.js's default scroll handling only brings the new Page element into view, |
49 | 10 | * which often resolves to "no scroll" inside shared layouts (see vercel/next.js#64435). |
50 | | - * |
51 | | - * Skipped on the very first mount of an initial page load (so browser scroll |
52 | | - * restoration on reload wins), when the pathname change closely follows a |
53 | | - * popstate (preserving browser back/forward restoration), and when a hash |
54 | | - * anchor is targeted (letting the browser's native anchor scroll win). |
| 11 | + * Skipped when a hash anchor is targeted so the browser's native anchor scroll wins. |
55 | 12 | */ |
56 | 13 | export function ScrollToTop() { |
57 | 14 | const pathname = usePathname() |
58 | 15 |
|
59 | 16 | useEffect(() => { |
60 | | - if (!hasMounted) { |
61 | | - hasMounted = true |
62 | | - if (wasInitialPageLoad) return |
63 | | - } |
64 | | - if (performance.now() - lastPopstateAt < POPSTATE_WINDOW_MS) { |
65 | | - lastPopstateAt = Number.NEGATIVE_INFINITY |
66 | | - return |
67 | | - } |
68 | 17 | if (window.location.hash) return |
69 | 18 | window.scrollTo(0, 0) |
70 | 19 | }, [pathname]) |
|
0 commit comments