From 85188522c7de1c9c75a54c45d56c1af3575bb374 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Mon, 20 Apr 2026 15:14:17 +0200 Subject: [PATCH] fix: list mode scroll fixing --- packages/shared/src/hooks/useFeed.ts | 2 + .../shared/src/hooks/useScrollRestoration.ts | 49 ++++++++++++++----- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/hooks/useFeed.ts b/packages/shared/src/hooks/useFeed.ts index 151094acfc9..80864c79b6e 100644 --- a/packages/shared/src/hooks/useFeed.ts +++ b/packages/shared/src/hooks/useFeed.ts @@ -19,6 +19,7 @@ import { getNextPageParam, removeCachedPagePost, RequestKey, + StaleTime, updateCachedPagePost, } from '../lib/query'; import type { MarketingCta } from '../components/marketingCta/common'; @@ -226,6 +227,7 @@ export default function useFeed( return res; }, refetchOnMount: false, + gcTime: StaleTime.OneHour, ...options, enabled: !!query && tokenRefreshed, refetchOnReconnect: false, diff --git a/packages/shared/src/hooks/useScrollRestoration.ts b/packages/shared/src/hooks/useScrollRestoration.ts index a6530f64ca4..36742da3c86 100644 --- a/packages/shared/src/hooks/useScrollRestoration.ts +++ b/packages/shared/src/hooks/useScrollRestoration.ts @@ -3,33 +3,58 @@ import { useEffect } from 'react'; import { useRouter } from 'next/router'; const scrollPositions: Record = {}; +const RESTORE_TIMEOUT_MS = 1000; + +const getScrollKey = (asPath: string): string => { + if (typeof window === 'undefined') { + return asPath; + } + const historyKey = (window.history.state as { key?: string } | null)?.key; + return historyKey ? `${asPath}:${historyKey}` : asPath; +}; export const useScrollRestoration = (): void => { - const { pathname } = useRouter(); + const { asPath } = useRouter(); useEffect(() => { const handleScroll = () => { - scrollPositions[pathname] = window.scrollY; + scrollPositions[getScrollKey(asPath)] = window.scrollY; }; - window.addEventListener('scroll', handleScroll); + window.addEventListener('scroll', handleScroll, { passive: true }); return () => { window.removeEventListener('scroll', handleScroll); }; - }, [pathname]); + }, [asPath]); useEffect(() => { - const scrollPosition = scrollPositions[pathname] || 0; + const target = scrollPositions[getScrollKey(asPath)] ?? 0; + if (target === 0) { + return undefined; + } + + // Wait until the page is tall enough before scrolling, so we don't clamp + // to the bottom while feed content is still hydrating. + const deadline = performance.now() + RESTORE_TIMEOUT_MS; + let frame = 0; + + const tick = () => { + const maxScroll = + document.documentElement.scrollHeight - window.innerHeight; + + if (maxScroll >= target || performance.now() >= deadline) { + window.scrollTo(0, Math.min(target, Math.max(0, maxScroll))); + return; + } + + frame = requestAnimationFrame(tick); + }; - // Add a small delay to ensure content is loaded before restoring scroll - // This is especially important for feed pages that load content dynamically - const timeoutId = setTimeout(() => { - window.scrollTo(0, scrollPosition); - }, 50); + frame = requestAnimationFrame(tick); - return () => clearTimeout(timeoutId); - }, [pathname]); + return () => cancelAnimationFrame(frame); + }, [asPath]); }; export const useManualScrollRestoration = (): void => {