fix: replace useResizeObserver with useLayoutEffect for reliable container width

useResizeObserver reads ref.current during render (before commit), so on
first render ref.current is null, no observation starts, and containerWidth
stays 0 if no subsequent re-render happens (e.g. page refresh with cached
SWR data). useLayoutEffect runs after refs are committed, so ref.current
is always the real DOM element.

This fixes both the right-column overflow (no window.innerWidth fallback
needed — width is always the actual container width) and the black screen
on refresh (containerWidth is reliable before the first paint).

https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
This commit is contained in:
Claude 2026-03-15 12:03:54 +00:00
parent 5e40dbbcd2
commit 067fdb50e1
No known key found for this signature in database

View File

@ -28,7 +28,7 @@ import {
VolumeState, VolumeState,
} from "@/types/live"; } from "@/types/live";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { useResizeObserver } from "@/hooks/resize-observer";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import useSWR from "swr"; import useSWR from "swr";
import { isDesktop, isMobile } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
@ -329,23 +329,25 @@ export default function DraggableGridLayout({
const gridContainerRef = useRef<HTMLDivElement>(null); const gridContainerRef = useRef<HTMLDivElement>(null);
const [{ width: containerWidth, height: containerHeight }] = const [containerWidth, setContainerWidth] = useState(0);
useResizeObserver(gridContainerRef); const [containerHeight, setContainerHeight] = useState(0);
// useResizeObserver reads ref.current at render time, so it may miss the // useLayoutEffect reads ref.current after commit (refs are set before layout
// initial mount when ref.current is null (e.g. on page refresh with cached // effects run), so this reliably fires before the first paint regardless of
// SWR data). Measure the container synchronously in useLayoutEffect as a // whether SWR triggers subsequent re-renders or not.
// reliable seed value; containerWidth from ResizeObserver takes over once
// it fires.
const [initialWidth, setInitialWidth] = useState(0);
useLayoutEffect(() => { useLayoutEffect(() => {
if (gridContainerRef.current) { const el = gridContainerRef.current;
setInitialWidth(gridContainerRef.current.offsetWidth); if (!el) return;
} setContainerWidth(el.clientWidth);
setContainerHeight(el.clientHeight);
const ro = new ResizeObserver(([entry]) => {
setContainerWidth(entry.contentRect.width);
setContainerHeight(entry.contentRect.height);
});
ro.observe(el);
return () => ro.disconnect();
}, []); }, []);
const effectiveWidth = containerWidth || initialWidth;
const scrollBarWidth = useMemo(() => { const scrollBarWidth = useMemo(() => {
if (containerWidth && containerHeight && containerRef.current) { if (containerWidth && containerHeight && containerRef.current) {
return ( return (
@ -356,8 +358,8 @@ export default function DraggableGridLayout({
}, [containerRef, containerHeight, containerWidth]); }, [containerRef, containerHeight, containerWidth]);
const availableWidth = useMemo( const availableWidth = useMemo(
() => (scrollBarWidth ? effectiveWidth + scrollBarWidth : effectiveWidth), () => (scrollBarWidth ? containerWidth + scrollBarWidth : containerWidth),
[effectiveWidth, scrollBarWidth], [containerWidth, scrollBarWidth],
); );
const hasScrollbar = useMemo(() => { const hasScrollbar = useMemo(() => {
@ -373,7 +375,7 @@ export default function DraggableGridLayout({
// subtract container margin, 1 camera takes up at least 4 rows // subtract container margin, 1 camera takes up at least 4 rows
// account for additional margin on bottom of each row // account for additional margin on bottom of each row
return ( return (
((availableWidth || window.innerWidth) - 2 * marginValue) / (availableWidth - 2 * marginValue) /
12 / 12 /
aspectRatio - aspectRatio -
marginValue + marginValue +
@ -724,7 +726,7 @@ export default function DraggableGridLayout({
currentGroups={groups} currentGroups={groups}
activeGroup={group} activeGroup={group}
/> />
{effectiveWidth > 0 && <Responsive {containerWidth > 0 && <Responsive
className="grid-layout" className="grid-layout"
width={availableWidth} width={availableWidth}
layouts={{ layouts={{