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.
This commit is contained in:
@@ -2937,6 +2937,13 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject<HTMLDivElem
|
||||
const ref = useRef<HTMLDivElement>(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<HTMLDivElem
|
||||
if (!on) return;
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
// The window is only ever set through here: a `setWin` the wall already
|
||||
// stands at is a render bought for nothing, and this is measured after every
|
||||
// render — that is the loop (React #185).
|
||||
const show = (next: { from: number; to: number; above: number; below: number }) => {
|
||||
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<HTMLElement>(':scope > .adm-card');
|
||||
const first = cards[0];
|
||||
@@ -2956,7 +2972,7 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject<HTMLDivElem
|
||||
// next pass read the step off what it drew.
|
||||
if (cards.length <= cols) {
|
||||
const to = Math.min(count, 24);
|
||||
setWin((prev) => (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<HTMLDivElem
|
||||
// last row it stands in for.
|
||||
const above = row0 ? row0 * pitch - WALL_GAP : 0;
|
||||
const below = row1 < rows ? (rows - row1) * pitch - WALL_GAP : 0;
|
||||
setWin((prev) =>
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user