Skip to content

fix(studio): break iframe ref update loop causing flickering#764

Open
vanceingalls wants to merge 1 commit into
feat/studio-build-devfrom
fix/studio-context-provider-loop
Open

fix(studio): break iframe ref update loop causing flickering#764
vanceingalls wants to merge 1 commit into
feat/studio-build-devfrom
fix/studio-context-provider-loop

Conversation

@vanceingalls
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls commented May 12, 2026

Summary

  • Break infinite update loop between handlePreviewIframeRef (App.tsx) and onIframeRef effect (NLELayout.tsx)
  • Callback identity change triggered the effect which re-called the callback, causing "Maximum update depth exceeded" in dev mode and intermittent flickering in production
  • Guard setPreviewIframe with identity check; use ref for onIframeRef callback in NLELayout effect deps

Test plan

  • Studio loads without "Maximum update depth exceeded" error in dev build
  • No flickering in production build
  • Sub-composition drill-down still works

🤖 Generated with Claude Code

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>
@vanceingalls vanceingalls force-pushed the fix/studio-context-provider-loop branch from 6d37ceb to eb8e446 Compare May 12, 2026 23:24
@vanceingalls vanceingalls changed the title fix(studio): Context.Provider for React 18 and iframe ref update loop fix(studio): break iframe ref update loop causing flickering May 12, 2026
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Identity guard in handlePreviewIframeRefif (previewIframeRef.current === iframe) return short-circuits the redundant state update.
  2. onIframeRefRef pattern 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

Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — clean two-prong fix for the update loop

Per Rule 5: pulled check_runs at eb8e446dmergeable_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 deps

The 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 === iframe guard): even if the callback runs, it no-ops when the iframe hasn't changed — prevents the setPreviewIframe setState 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants