fix(ui): harden board canvas against React #185 render-loop crash#1746
Open
kgabryje wants to merge 3 commits into
Open
fix(ui): harden board canvas against React #185 render-loop crash#1746kgabryje wants to merge 3 commits into
kgabryje wants to merge 3 commits into
Conversation
React #185 ("Maximum update depth exceeded") can fire on the board canvas when the tab regains visibility after an OS space/desktop switch (ResizeObserver re-measurement). The only ErrorBoundary was the global one, so a canvas render loop white-screened the entire app. Add a canvas-scoped ErrorBoundary around <SessionCanvas> with a new "canvas" variant: it contains the crash to the canvas subtree, keeps the app shell (header, panels, nav) alive, and offers an in-place "Reload canvas" reset instead of a full page reload. resetKey clears a stuck error when the user switches boards. Crash reporting reuses the existing crashReport helpers so auto-issue reporting is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two zone-toolbar ColorPickers are always mounted (visibility is CSS-only to avoid mount/unmount flicker), so a fresh `presets` array on every render churns antd's picker internals and its rc-resize-observer machinery — feeding the ResizeObserver re-measurement loop behind React #185. Memoize the derived color palette and both `presets` arrays so their references stay stable across renders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifies the canvas-variant boundary contains a child render error while the surrounding shell stays mounted, and that the "Reload canvas" action re-mounts the subtree in place once the error condition clears. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Member
Author
🤖 Automated review — Claude Opus 4.8Verdict: Approve (defense-in-depth). This hardens the blast radius of the #1738/#1740 render-loop crash; it isn't (and doesn't claim to be) a root-cause fix. Reviewed against head Deliverable 1 — canvas ErrorBoundary (correct)
Deliverable 2 — ColorPicker preset memoization (correct)
Notes (non-blocking)
LGTM to merge as hardening once CI is green. |
Member
Author
🤖 Automated review — Codex (GPT-5.5)Verdict: Mergeable as defense-in-depth hardening — no correctness/regression blockers. (head Verified:
Minor (non-blocking):
|
mistercrunch
approved these changes
Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
React #185 ("Maximum update depth exceeded") can fire on the board canvas
when the browser tab regains visibility after an OS desktop/space switch —
the ResizeObserver re-measures and a render loop starts (issues #1738, #1740).
Because the only
ErrorBoundarywas the global one, this white-screened theentire app.
What changed
Deliverable 1 — canvas-scoped ErrorBoundary (primary,
7f4d79e1+ test8570e41c)variant="canvas"on the existingErrorBoundary, wrapping<SessionCanvas>in
components/App/App.tsx.stays alive instead of white-screening.
CanvasCrashScreenfallback offers an in-place "Reload canvas" reset(clears boundary state, re-mounts the subtree — no full page reload) plus
"Copy error details" / "Report on GitHub".
crashReporthelpers, so the auto-issue reporting paththat produced UI crash: unknown component — Minified React error #185; visit https://react.dev/errors/185 for the full messa #1738/UI crash: unknown component — Minified React error #185; visit https://react.dev/errors/185 for the full messa #1740 is preserved.
resetKey={currentBoard?.board_id}clears a stuck error when switching boards.Deliverable 2 — reduce the loop surface (secondary,
e2904863)<ColorPicker>s rebuilt theirpresetsarrays on every render, churning antd's picker + rc-resize-observer internals.
presetsarrays so referencesstay stable across renders. Kept the ColorPickers always-mounted (CSS-only
visibility) to avoid reintroducing the mount/unmount flicker the current code
guards against.
Verification
pnpm typecheck(agor-ui): 0 errors after building the@agor-live/clientand
@agor/coreworkspace deps (fresh worktree had nodist).biome checkon changed files: clean.vitest(ErrorBoundary + SessionCanvas): 25 passed (23 existing + 2 new).Not done / honest caveats
jsdom — that timing-dependent loop isn't reliably reproducible in a unit test.
Deliverable 1 is verified defense-in-depth (the boundary provably contains a
render crash); Deliverable 2 is a targeted reduction of a known re-render
source, not a proven root-cause fix.
PRs if desired (the test commit belongs with Deliverable 1).
🤖 Generated with Claude Code