refactor(studio): compile the remaining hooks under the React Compiler - #3777
Draft
miguel-heygen wants to merge 2 commits into
Draft
Conversation
Latest-value ref writes move out of render into commit effects, `try`/`finally` in hook bodies becomes a plain helper or a promise finalizer, and three dependency lists are made true so their suppressions can go. Where a value is read changes; what happens does not. Lowers the bail-out baseline for these 22 files to zero.
…hanged `useLintModal` lost a `finally` clause and `useMusicBeatAnalysis` moved a ref write into an effect that has to run before the loader. Neither had a test. Both new suites were watched failing with the change backed out.
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 C2b (hooks, second half) of the Studio React Compiler coverage plan. Stacked on the ratchet PR (#3773). Ratchet 126 to 104. The stack diff shown here includes C1 until it lands.
What
The second half of the Studio hooks now compile under the React Compiler, with their bail-out ratchet entries at zero.
Baseline total: 126 to 104. Twenty-two files, each 1 to 0:
hooks/useLintModal.tstry/finallycatchnever rethrows, so thefinallybody becomes the statement after thetry/catchuseLintModal.test.tsx, 3 cases, watched failing with the flag reset reachable on one path onlyhooks/useLivePlayheadTime.tsnullmeans "nothing from this run yet", so the first playing frame falls back to the store exactly as the ref sync diduseLivePlayheadTime.test.tsx, 4 cases including the stale-previous-run one, watched failing with the reset removedhooks/useMountEffect.tsreact-hookssuppressionuseRef, so the empty dependency list is true;useRefkeeps its initial value, so it is still the first render's effecthooks/useMusicBeatAnalysis.tsuseMusicBeatAnalysis.test.tsx, 3 cases, watched failing with the refreshing effect moved after the loaderhooks/usePanelLayout.tseffectiveLeftCollapsedRefis seeded with the real first-render value instead offalseusePanelLayout.test.tshooks/usePersistentEditHistory.tsusePersistentEditHistory.test.ts,usePersistentEditHistory.projectOwnership.test.tsxhooks/usePreviewPersistence.tsusePreviewPersistence.test.ts,externalFileChangeOwnership.test.ts, studio load smokehooks/useProjectCompositionVariables.tsreact-hookssuppression, thentry/finallyfinallyand allhooks/useProjectSignaturePoll.tsuseProjectSignaturePoll.test.tsxhooks/useRazorSplit.ts??=written longhand with the same nullish testuseRazorSplit.test.ts,useRazorSplit.test.tsx,useRazorSplit.history.test.tsxhooks/useRemoveBackground.tstry/finallywithoutcatchfinallyand allhooks/useSdkSession.tsuseSdkSession.test.ts,useSdkSession.lifecycle.test.tsxhooks/useSlideshowTabState.tsreact-hookssuppression (over a ref read in a memo)useSlideshowTabState.test.ts, 3 new cases for scene derivation, empty manifest and a throwing framehooks/useThumbnailLease.tsgetSnapshottakes the request from its arguments (it is read during render);subscribekeeps the identity-keyed ref, refreshed on commit before React subscribesuseThumbnailLease.test.tsx, including "resubscribes when the request work shape changes"hooks/useTimelineDeleteOps.tsthrowinsidetry/catch(three).catchuseTimelineEditing.test.tsx, "rolls back the store duration and live root when a delete persist fails"hooks/useTimelineEditing.tsuseTimelineEditing.test.tsxhooks/useTimelineGroupEditing.tstry/finallywithoutcatch(two).finallyon the promise; the callee isasync, so it can only rejectuseTimelineEditing.test.tsx, the group-move and group-resize cases that assert the GSAP cache is invalidated on both the fallback and track-only pathshooks/useTimelineSelectionPreviewSync.tsreact-hookssuppressionuseTimelineSelectionPreviewSync.test.tsxhooks/useToast.tsplayer/hooks/usePlaybackKeyboard.tsusePlaybackKeyboard.test.ts, which dispatches through the refplayer/hooks/useTimelinePlayer.ts??=longhand; the store's action handles are read through a plain function; the adapter ref moves to a commit effectuseTimelinePlayer.test.ts,useTimelinePlayer.seek.test.ts, timeline viewport gatewebmcp/useStudioAgentTools.tsuseStudioAgentTools.test.tsxWhy
react({ compiler: true })compiles what it can and silently emits the rest as written, so a hook that opts itself out costs nothing visible and nothing catches it. The per-file ratchet turns that silence into a number; this unit takes twenty-two of them to zero so the number can eventually reach zero and stay there.Every one of these bail-outs is also a real React rule being bent. A ref written during render is a value the compiler cannot see change, and a render that is thrown away still writes it. Moving the write to the commit is the correction, not a workaround for the compiler.
How
Three shapes, applied at the root rather than suppressed:
useMusicBeatAnalysissuite pins exactly that ordering. Two cases needed more than a move:useThumbnailLeasereads its request during render, sogetSnapshottakes it from its arguments and onlysubscribekeeps the ref;usePreviewPersistencebuilds its save queue on first use through one accessor, because the compiler follows a ref through any function render calls, including auseStateinitializer.try/finallyin a hook body extracted to a plain function, or replaced by.finallyon the promise where a singleawaitwas being guarded. InuseLintModalno extraction was needed at all: itscatchswallows rather than rethrows, so thefinallybody is simply the next statement.No
"use no memo", no new lint suppression, noref.currentmirrored into state.Two incidental cleanups that fell out:
usePreviewPersistencestopped returning a queue ref nothing outside it read, anduseTimelineDeleteOpsshares one dependency interface between the hook and the extracted function instead of repeating nine fields.Test plan
PASS: studio loaded with schema-valid API fixtures and no runtime errors.Not covered
useProjectCompositionVariablesanduseRemoveBackgroundeach moved atry/finallyblock verbatim into a plain function, anduseToastmoved a counter increment into one; the guarded statements and thefinallyclauses are unchanged, so there is no branch to pin that did not exist before. Neither hook has a test today and this change does not add one..finallyconversions inuseTimelineGroupEditingassume the callee cannot throw before returning its promise. That holds because it is declaredasync; it is not enforced by a type.useMountEffectnow holds the first render's closure rather than the committed render's. Under a discarded render both closures compute from the same inputs, so this is not observable, but it is a difference on paper.react-hooks/exhaustive-deps(it is not in the correctness category), so removing three suppressions was checked against the compiler and by running that rule explicitly, not by the standard lint gate.