refactor(studio): compile the GSAP and gesture hooks under the React Compiler - #3784
Draft
miguel-heygen wants to merge 1 commit into
Draft
Conversation
The second half of the hooks folder, same causes and same rule: fix where a value is read, never what happens. Ref writes move into an effect or drop when the ref was standing in for a memo, guarded control flow moves to a plain function or a promise handler. useGsapTweenCache needed more than a local edit. Removing its two react-hooks suppressions uncovered what they were hiding: two memos and an effect resolving the live preview DOM through a ref parameter, which can never be a true dependency. Both hooks now take the iframe element the caller already holds as state, so the reads are of a value and the dependency lists are true without a suppression. The one argument that leaves dead behind it is removed from its caller. The bail-out baseline falls from 115 to 104, one per file. No hook under src/hooks bails out any more.
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.
Lands unit C2a, second half (hooks) of the Studio React Compiler coverage plan. Stacked on the first half. Ratchet 115 to 104; no src/hooks entry remains. About 190 lines over the usual size guideline: a relocated commit body accounts for most of it, unedited.
What
The remaining eleven hooks under
packages/studio/src/hooks,useElementPicker.tsthroughuseGsapTweenCache.tsin path order, now compile under the React Compiler. The per-file bail-out baseline drops from 115 to 104. No hook undersrc/hooksbails out any more.Two files move with them:
useDomEditWiring.ts, which calls the two hooks whose parameter changed, anduseDomEditSession.ts, which loses the one argument that became dead.Three new test files and three added cases pin the paths whose structure changed.
Stacked on
refactor/studio-c2a1-compiler-hooks-a, which takes the first eleven.Why
react({ compiler: true })is silent about what it declines. A hook the compiler skips is emitted exactly as written, so losing every memo inside it costs nothing you can see in a diff, a build log, or a test run.How
One row per file: the cause the scan reported, the shape of the fix, and the test that holds it.
useElementPicker.tsuseElementPicker.test.tsx(3 cases incl. the zoomed-frame override and the newest options)useExternalFileChangeCoordinator.tsuseExternalFileChangeCoordinator.test.tsx,externalFileChangeOwnership.test.tsuseFileManager.tsuseFileManager.projectOwnership.test.tsx,useEditorSave.test.tsxuseFrameCapture.tstry/finally, threethrows intry/catchuseFrameCapture.test.tsx(4 cases: download, server message, status fallback, save-queue failure)useGestureCommit.ts++on a module binding;try/finallyfinallyis the caller's cleanup. The counter gets a module-scope accessoruseGestureCommit.test.tsx+ new case: the recording is released when the commit rejectsuseGestureRecording.tsuseGestureRecording.test.tsx+ new assertion on the live alias mid-gestureuseGsapAwareEditing.ts++on a module binding; pattern in afor..ofinituseGsapAwareEditing.test.tsx,gsapDragCommit.test.ts(33 cases)useGsapPropertyDebounce.tsuseGsapPropertyDebounce.test.ts,useGsapPropertyDebounceFlush.test.tsuseGsapScriptCommits.tsuseGsapScriptCommits.test.tsxuseGsapSelectionHandlers.tsuseGsapSelectionHandlers.test.tsx+ new case: an edit after the selection clears still targets the last elementuseGsapTweenCache.tsreact-hookssuppressionsuseGsapAnimationsForElement.test.tsx(3 cases)The one that needed more than a local edit
Removing
useGsapTweenCache's two suppressions uncovered what they were hiding: two memos and an effect resolving the live preview DOM through a ref parameter, which can never be a true dependency, which is why the omission had to be suppressed to stand.Both hooks now take the iframe element the caller already holds as state. The reads are of a value, the dependency lists are true without a suppression, and a replaced iframe re-resolves instead of waiting for the next cache-version bump. The preview document is also tagged with the composition generation that produced it, which is what the extra
versiondependency was standing in for: a soft reload swaps the document inside the same element, so the element alone cannot say the DOM changed.That left one argument dead in
useDomEditWiring, removed here along with the line that passed it.A third bail-out surfaced underneath (
++on a variable an interval callback captures) and moved to a module-scope poll helper.Three notes on fix shapes, measured against this toolchain
useLatestRefhelper does not work. The compiler recognises a ref by theuseRefcall site, not by the shape of what a helper returns, so a hook reading.currentfrom a helper's return insideuseCallback([])loses its memoization instead of gaining it. The write stays inline in each hook.getin an object expression is a lowering the compiler declines..catch()and.finally()on a promise are fine wheretry/catchandtry/finallystatements are not, which is what keeps several of these fixes inside their hooks instead of moving 200 lines to module scope.Test plan
bunx vitest run src/styles/compilerBailouts.test.ts --maxWorkers=4, 14 passed, baseline 115 down to 104, exactly these eleven entries removed. Nosrc/hooksentry remains.bunx vitest run --maxWorkers=4, 436 passed, 1 skipped (437 files); 4784 passed, 18 todo. The branch below is 433 files and 4772 tests; this adds 3 files and 12 cases.bun run typecheck, clean.oxlintandoxfmt --checkon the 19 changed source files, from the repo root, clean.fallow audit --base origin/main --fail-on-issues, passes. Two complexity exemptions move with the code they were already granted for; the three remaining findings are inherited, in functions this stack does not touch.studio-load-smokecommand, run locally on the combined tree:PASS: studio loaded with schema-valid API fixtures and no runtime errors.studio-timeline-viewportgate, both arms, run locally on the combined tree: 5/5 passing runs each. Virtualization on: interaction p95 32.4ms, frame interval p95 16.7ms. Virtualization off: 32.8ms / 16.8ms. Budget 75ms.finallyor catch was watched failing with that clause broken on purpose.Not covered
useElementPicker's active-iframe alias, which is now refreshed after commit rather than during render, so it reads the iframe after React has attached it instead of before.useElementPickeris exported from the package root and its own signature is unchanged. The two hooks whose parameter changed are internal to this package.useGsapTweenCachebefore this branch, so the new one is the first thing pinning its live-DOM resolution. It covers attribution through the preview document, not the runtime keyframe enrichment below it.