From fa2e9e133ef0778ebc1690d1bf70885243a14dc4 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 9 Oct 2026 17:10:44 +0700 Subject: [PATCH] fix(library): stop the wall's window loop that blanked the screen Selecting thumbs and flipping SINGLE <-> ALL THUMBS crashed the view with React #185 ("Maximum update depth exceeded") and left the app white. useWallWindow measures after every render through a layout effect with no dependency array, and it ended by setting state with a value it had just found. A set made from a layout effect renders again whether or not the value moved, so an unchanged measurement re-rendered, which remeasured, which set again: the loop. It is now guarded on the last window actually written (a ref), and nothing is set that the wall already stands at. Reproduced with the throwaway storm probe against a 3000-file roll (the tree vanished at round 2, ~50 identical renders). After the fix: 12 rounds of hard SINGLE/ALL THUMBS toggling on the production bundle, no error, no white screen. --- docker/frontend/src/Library.tsx | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index dd083a0..45101d5 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -2937,6 +2937,13 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject(null); // The first window is a screenful: the wall has not been measured yet. const [win, setWin] = useState(() => ({ from: 0, to: Math.min(count, 24), above: 0, below: 0 })); + // The window last written, because the reader of a `setWin` in a layout effect + // is not the state: a set made from a layout effect renders again whether or + // not the value moved, and this one is remeasured after every render — so a set + // it did not need is the screen's own render loop (React #185, "Maximum update + // depth exceeded"). The ref is the guard: nothing is set that the wall already + // stands at. + const winRef = useRef(win); const measureRef = useRef<() => void>(() => {}); // The window over a scroll container is taken rather than kept, so it is taken // after every render: the first pass draws a screenful, the next reads the row's @@ -2947,6 +2954,15 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject { + const prev = winRef.current; + if (prev.from === next.from && prev.to === next.to && prev.above === next.above && prev.below === next.below) return; + winRef.current = next; + setWin(next); + }; const measure = () => { const cards = el.querySelectorAll(':scope > .adm-card'); const first = cards[0]; @@ -2956,7 +2972,7 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject (prev.from === 0 && prev.to === to && !prev.above && !prev.below ? prev : { from: 0, to, above: 0, below: 0 })); + show({ from: 0, to, above: 0, below: 0 }); return; } // The step from one row to the next, taken as the drawn run's own average. @@ -2978,9 +2994,7 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject - prev.from === from && prev.to === to && prev.above === above && prev.below === below ? prev : { from, to, above, below } - ); + show({ from, to, above, below }); }; measureRef.current = measure; const onScroll = () => measure();