fix(studio): break iframe ref update loop causing flickering#764
fix(studio): break iframe ref update loop causing flickering#764vanceingalls wants to merge 1 commit into
Conversation
Fixes an infinite update loop between handlePreviewIframeRef and NLELayout's onIframeRef effect — the callback identity change triggered the effect which re-called the callback, creating a render loop visible as "Maximum update depth exceeded" in dev mode and intermittent flickering in production. - Guard setPreviewIframe with identity check (skip if same iframe) - Use ref for onIframeRef in NLELayout effect to avoid re-firing on callback identity change Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6d37ceb to
eb8e446
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Solid fix. The infinite loop diagnosis is spot-on: handlePreviewIframeRef callback → setPreviewIframe → re-render → new callback identity → onIframeRef effect re-fires → calls the callback again.
Both halves of the fix are correct:
- Identity guard in
handlePreviewIframeRef—if (previewIframeRef.current === iframe) returnshort-circuits the redundant state update. onIframeRefRefpattern in NLELayout — stable ref removes the callback from effect deps, breaking the cycle from the consumer side too.
Belt-and-suspenders approach means either fix alone would stop the loop, but having both is defensive in the right way. No functional behavior change for the happy path.
LGTM.
— Magi
jrusso1020
left a comment
There was a problem hiding this comment.
Verdict: APPROVE — clean two-prong fix for the update loop
Per Rule 5: pulled check_runs at eb8e446d — mergeable_state: "clean", all 11 checks completed (preview-regression, Preview parity, player-perf, regression, Detect changes ×3, Mintlify skipped, WIP). No failures, no File size check failure (this PR doesn't touch >500 LOC files), no Windows red (Windows checks didn't run on this stacked PR).
Audited
Both files end-to-end (3 lines total of substantive change).
App.tsx:264 — adds if (previewIframeRef.current === iframe) return; as the first line of handlePreviewIframeRef. Identity-check early return: when React's ref-callback fires with the same iframe instance (which happens on every re-render of the parent component that hosts the iframe), the callback no-ops instead of cascading state updates.
NLELayout.tsx:237-240 — the classic stable-listener-mutable-handler pattern:
const onIframeRefRef = useRef(onIframeRef);
onIframeRefRef.current = onIframeRef;
useEffect(() => {
onIframeRefRef.current?.(iframeRef.current);
}, [compositionStack.length, refreshKey, iframeRef]); // ← onIframeRef removed from depsThe effect previously re-ran whenever onIframeRef's identity changed (every render of the parent that didn't memoize the callback). Now it only re-runs on compositionStack.length / refreshKey / iframeRef — the actual semantic triggers — while still calling the latest onIframeRef via the ref.
Why this fixes the loop
The bug chain (per PR description): parent re-render → new handlePreviewIframeRef identity → NLELayout's effect dep changes → effect re-runs → calls parent's callback → parent setState fires (setPreviewIframe) → parent re-renders → repeat. "Maximum update depth exceeded" in dev mode, intermittent flickering in production.
Both fixes break the loop at different layers:
- App.tsx (
previewIframeRef.current === iframeguard): even if the callback runs, it no-ops when the iframe hasn't changed — prevents thesetPreviewIframesetState that propagates back. - NLELayout.tsx (ref-based callback): even if the callback identity changes upstream, the effect doesn't re-fire — breaks the trigger condition.
Defense in depth. Either fix alone might have been sufficient, but landing both makes the loop unrepeatable through future refactors that might otherwise re-introduce the same shape (e.g. someone removing the App.tsx guard without realizing the effect would then re-fire).
Praise
- Stable-listener-mutable-handler pattern is the textbook React fix. Already established in
useAppHotkeys(per the hf#741 review trail). Applying it here is consistent with team patterns. - Identity-check early return at the callback entry is the cheapest possible guard — no allocations, no closures, just a reference comparison.
- Both layers fixed, not just the symptom — Vance's PR description correctly explains the chain. The README-shape "what changed + why" framing is exactly right for a non-trivial fix like this.
- Test plan items target the actual symptoms (dev-mode error message, prod-mode flickering, sub-composition drill-down still working). Properly framed.
mergeable_state: "clean" — no observations on CI gating, ready to ship after stack lands.
Review by Rames Jusso (pr-review)
Summary
handlePreviewIframeRef(App.tsx) andonIframeRefeffect (NLELayout.tsx)setPreviewIframewith identity check; use ref foronIframeRefcallback in NLELayout effect depsTest plan
🤖 Generated with Claude Code