From 8cbaf1d731445bf948938eb6b24de0c15909bb10 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 12 Jul 2026 00:09:19 +0200 Subject: [PATCH] =?UTF-8?q?fix(plex):=20infinite-scroll=20dead=20after=20d?= =?UTF-8?q?rill-in=20=E2=86=92=20Back=20(stale=20sentinel=20observer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unified-library grid unmounts when you open an item's info/show/season page and remounts as a NEW element on Back. The IntersectionObserver effect keyed only on [hasNextPage, isFetchingNextPage, fetchNextPage] — none of which change across a pure navigation — so it never re-ran: the observer kept watching the detached old sentinel node and fetchNextPage never fired again, leaving the feed frozen at the already-loaded pages (e.g. 80/1227) even though more exist. Intermittent because a coincident isFetchingNextPage toggle around the nav could re-run the effect. Fix: observe via a callback ref (state) instead of useRef, so the effect re-runs on every sentinel mount/unmount and always watches the live node. --- frontend/src/components/PlexBrowse.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 82bdf25..9696148 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -156,21 +156,24 @@ export default function PlexBrowse({ // Episode matches (grouped "Episodes" section) — search-only; same set on every page, take page 0. const episodes = browseQ.data?.pages[0]?.episodes ?? []; - // Infinite scroll: auto-load the next page when the sentinel scrolls into view. - const sentinel = useRef(null); + // Infinite scroll: auto-load the next page when the sentinel scrolls into view. The sentinel lives + // in the grid branch, which UNMOUNTS when we drill into an info/show/season page and remounts on + // Back — as a NEW element. A callback ref (state, not useRef) re-runs this effect on every + // mount/unmount, so the observer always watches the live node; a plain ref would keep observing the + // detached old node after Back and never fire fetchNextPage again (feed stuck at the loaded pages). + const [sentinelEl, setSentinelEl] = useState(null); const { hasNextPage, isFetchingNextPage, fetchNextPage } = browseQ; useEffect(() => { - const el = sentinel.current; - if (!el) return; + if (!sentinelEl) return; const io = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage(); }, { rootMargin: "600px" }, ); - io.observe(el); + io.observe(sentinelEl); return () => io.disconnect(); - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + }, [sentinelEl, hasNextPage, isFetchingNextPage, fetchNextPage]); function onCard(card: PlexCard) { scrollRef.current = scroller()?.scrollTop ?? 0; // remember grid position for the trip back @@ -345,7 +348,7 @@ export default function PlexBrowse({ )} -
+
{isFetchingNextPage &&

{t("plex.loading")}

}
);