From 76358a0d77a52928278dd94be87db41b9879bc29 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 3 Oct 2026 23:18:26 +0200 Subject: [PATCH] refactor(ui): the popover engine and every render-phase ref write compile under the React Compiler E32 S3. The compiler skipped 93 functions; it now skips 29 (try/finally shapes, two suppressions, VirtualGrid). react-hooks/refs hits 245 -> 31. - Popover engine: `ref={pop.refs.setReference}` made the compiler treat the whole popover handle as a ref, so every component holding one was skipped (21 call sites, the engine and its surface among them). The setter is destructured now; the same for useScrollFade and useReturnFocus. - Render-phase `ref.current = x` writes moved to `useEffectEvent` where only effects read them, else to the new `useLatest` (written in a layout effect) / `useStableCallback`. - FocusSafeButton (under every Button) read the focused element during render; it reads it through useSyncExternalStore, which React asks on every render instead of a cached memo. Bugs this surfaced, fixed: - Combobox: a chip could keep showing its raw id. The label cache was a ref an effect filled after the render that needed it; it is state merged during render now (test seen RED). - useUndoable rendered from a ref plus a forced re-render; compiled, that memoizes a stale value. The history is state now, mirrored in a ref for the stable callbacks. - ConfigPanel and the saved-views editor re-seeded their drafts in an effect, painting the stale draft for a frame; both adjust during render against the remembered key. - Toaster's handover is written after commit instead of during render; a new test pins that the inherited set is taken once, at the takeover (seen RED on a per-render re-read). - IssueComposer mutated a ref prop as a one-shot focus token; it is plain state. - PlexBrowse wrote its info-scroll memory from an effect declared before the ref, past its own "one writer" rule; the writer comes first now and the effect goes through it. The compiler ratchet drops to the measured 29 and now also requires FocusSafeButton and the popover engine to compile. --- frontend/src/components/AboutCell.tsx | 4 +- frontend/src/components/ActiveFilterChips.tsx | 3 +- frontend/src/components/AddToPlaylist.tsx | 9 ++- frontend/src/components/BootGate.tsx | 9 +-- .../src/components/ChannelStatsPopover.tsx | 3 +- frontend/src/components/ChannelTags.tsx | 13 +-- frontend/src/components/ChannelsProvider.tsx | 7 +- frontend/src/components/ChatThread.tsx | 25 +++--- frontend/src/components/ConfigPanel.tsx | 38 +++++---- frontend/src/components/DateFilterSelect.tsx | 4 +- frontend/src/components/Feed.tsx | 18 ++--- frontend/src/components/IssueComposer.tsx | 37 ++++----- frontend/src/components/LanguageSwitcher.tsx | 4 +- frontend/src/components/Messages.tsx | 12 +-- frontend/src/components/NavSidebar.tsx | 7 +- .../src/components/NavigationProvider.tsx | 19 ++--- frontend/src/components/OnboardingWizard.tsx | 8 +- .../src/components/PersonalViewsModal.tsx | 60 ++++++++------ frontend/src/components/PlayerModal.tsx | 14 ++-- .../src/components/PlaylistLimitsHint.tsx | 5 +- frontend/src/components/Playlists.tsx | 52 ++++++------ frontend/src/components/PlexBrowse.tsx | 28 +++---- .../src/components/PlexCollectionEditor.tsx | 12 +-- .../src/components/PlexCollectionView.tsx | 8 +- frontend/src/components/PlexPlayer.tsx | 6 +- frontend/src/components/PlexPlaylistAdd.tsx | 6 +- frontend/src/components/PlexPlaylistView.tsx | 8 +- frontend/src/components/SavedViewsPopover.tsx | 5 +- frontend/src/components/SyncStatus.tsx | 3 +- frontend/src/components/TagFilterChips.tsx | 3 +- frontend/src/components/TagManager.tsx | 15 ++-- frontend/src/components/UpNextCard.tsx | 9 +-- frontend/src/components/VideoEditor.tsx | 25 +++--- frontend/src/components/ViewSwitcher.tsx | 4 +- frontend/src/components/Welcome.tsx | 9 +-- frontend/src/components/YearFilter.tsx | 4 +- .../compilerCoverage.contract.test.ts | 80 ++++--------------- frontend/src/components/ui/ActionMenu.tsx | 8 +- .../src/components/ui/AnchoredPopover.tsx | 7 +- .../src/components/ui/Combobox.dom.test.tsx | 24 ++++++ frontend/src/components/ui/Combobox.tsx | 58 +++++++++----- frontend/src/components/ui/FilterSelect.tsx | 3 +- .../src/components/ui/FocusSafeButton.tsx | 23 +++++- frontend/src/components/ui/Modal.tsx | 8 +- frontend/src/components/ui/NavRail.tsx | 3 +- frontend/src/components/ui/PageScroller.tsx | 50 +++++++----- .../src/components/ui/PriorityStepper.tsx | 8 +- frontend/src/components/ui/SidePanel.tsx | 6 +- frontend/src/components/ui/SortControl.tsx | 3 +- .../src/components/ui/Toaster.dom.test.tsx | 12 +++ frontend/src/components/ui/Toaster.tsx | 41 +++++----- frontend/src/components/ui/Tooltip.tsx | 3 +- .../src/components/ui/anchoredPopoverHooks.ts | 11 +-- frontend/src/components/ui/sortableA11y.ts | 23 +++--- frontend/src/components/ui/tableHeader.tsx | 9 ++- frontend/src/lib/history.ts | 17 ++-- frontend/src/lib/keymap.ts | 11 ++- frontend/src/lib/layerStack.ts | 24 +++--- frontend/src/lib/listNav.ts | 14 ++-- frontend/src/lib/playerModalHooks.ts | 4 - frontend/src/lib/plexPlayerHooks.ts | 19 ++--- frontend/src/lib/useChannelPriority.ts | 39 ++++----- frontend/src/lib/useChipOverflowFit.ts | 9 +-- frontend/src/lib/useDismiss.ts | 20 ++--- frontend/src/lib/useLatest.ts | 26 ++++++ frontend/src/lib/usePref.ts | 11 +-- frontend/src/lib/useUndoable.ts | 66 ++++++++------- frontend/src/lib/useUrlMirror.ts | 15 ++-- 68 files changed, 606 insertions(+), 545 deletions(-) create mode 100644 frontend/src/lib/useLatest.ts diff --git a/frontend/src/components/AboutCell.tsx b/frontend/src/components/AboutCell.tsx index 00895088..b4412c94 100644 --- a/frontend/src/components/AboutCell.tsx +++ b/frontend/src/components/AboutCell.tsx @@ -36,6 +36,8 @@ export default function AboutCell({ text, channelId }: { text: string | null; ch fitHeight: true, }); + const { setReference } = pop.refs; + const { data: ch } = useQuery({ queryKey: qk.channel(channelId), queryFn: () => api.channelDetail(channelId), @@ -48,7 +50,7 @@ export default function AboutCell({ text, channelId }: { text: string | null; ch return ( <>