From f1385fdaab8cd13154a3ffa5e1c18541411a60f0 Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Fri, 19 Jun 2026 15:50:51 +0200 Subject: [PATCH 1/3] fix(reports): stop grid layout update loop RGL fires onLayoutChange on every layout-prop change (recreated each render), and it always called setLayout, feeding an endless setLayout -> render -> onLayoutChange cycle (Maximum update depth). Skip the update when no tile actually moved or resized. --- src/components/reports/ReportCanvas.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/reports/ReportCanvas.tsx b/src/components/reports/ReportCanvas.tsx index f93aa2d..90405e1 100644 --- a/src/components/reports/ReportCanvas.tsx +++ b/src/components/reports/ReportCanvas.tsx @@ -194,6 +194,19 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { }) const onLayoutChange = (current: RglItem[]) => { + // RGL fires this on every layout-prop change (which we recreate each + // render). Bail out when nothing actually moved/resized, otherwise the + // setLayout -> re-render -> new layout prop -> onLayoutChange cycle never + // settles (Maximum update depth exceeded). + const prevById = new Map(layout.map((l) => [l.i, l])) + const unchanged = + current.length > 0 && + current.every((c) => { + const p = prevById.get(c.i) + return p && p.x === c.x && p.y === c.y && p.w === c.w && p.h === c.h + }) + if (unchanged) return + // Keep hidden sections' saved positions so toggling them back never loses placement. const visibleIds = new Set(current.map((l) => l.i)) const preserved = layout.filter((l) => !visibleIds.has(l.i)) From 82491b87d199f5b11e1c49a1d2e5852a84709a05 Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Fri, 19 Jun 2026 15:51:44 +0200 Subject: [PATCH 2/3] chore(dev): add opt-in turbopack dev script dev:turbo runs next dev --turbopack for faster cold compiles; plain dev stays on webpack as the safe default (react-grid-layout uses require). --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6157558..23e88bf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "scripts": { "predev": "prisma migrate status || echo '\\n[!] Pending migrations or DB unreachable. If the app errors on a missing table, run: npm run db:migrate\\n'", "dev": "next dev", + "dev:turbo": "next dev --turbopack", "build": "next build", "start": "next start", "lint": "eslint .", From 7088a59f2447db0a70af06dfb726759ec55779a6 Mon Sep 17 00:00:00 2001 From: Melvin PETIT Date: Fri, 19 Jun 2026 16:03:48 +0200 Subject: [PATCH 3/3] fix(reports): dedupe container width to break resize loop contentRect.width is fractional and jitters sub-pixel as tiles auto-fit; since containerW is a fitHeights effect dependency, the raw value changed every render and never settled. Round and dedupe it. --- src/components/reports/ReportCanvas.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/reports/ReportCanvas.tsx b/src/components/reports/ReportCanvas.tsx index 90405e1..8c576a4 100644 --- a/src/components/reports/ReportCanvas.tsx +++ b/src/components/reports/ReportCanvas.tsx @@ -91,7 +91,14 @@ export function ReportCanvas({ sections }: { sections: ReportSectionEntry[] }) { useEffect(() => { const el = containerRef.current if (!el) return - const ro = new ResizeObserver((entries) => setContainerW(entries[0].contentRect.width)) + // Round and dedupe: contentRect.width is fractional and jitters by + // sub-pixels as tiles auto-fit, and containerW is a dependency of the + // fitHeights effect. An unrounded value changes every render and feeds an + // endless update loop (Maximum update depth exceeded). + const ro = new ResizeObserver((entries) => { + const w = Math.round(entries[0].contentRect.width) + setContainerW((prev) => (prev === w ? prev : w)) + }) ro.observe(el) return () => ro.disconnect() }, [])