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.
This commit is contained in:
@@ -143,12 +143,22 @@ export default function PlexBrowse() {
|
||||
}
|
||||
}, [openPlaylist, sub, setPlaylistOpen]);
|
||||
|
||||
// Like the grid's scroll memory (below), for the info page: "Browse collection" (onFilter) leaves
|
||||
// the info view for a filtered grid, and browser Back returns to this same info page — restore
|
||||
// where the user had scrolled it (down to a collection strip) instead of snapping to the top. Reset
|
||||
// on a fresh info open so a different title's info starts at the top.
|
||||
const infoScrollRef = useRef(0);
|
||||
// ONE writer for that memory, so "stash on the way out of an info page, reset on a fresh one" is a
|
||||
// rule the code holds rather than one spread over the call sites. Declared above the effects that
|
||||
// use it: an effect closing over the ref before its declaration made the compiler skip PlexBrowse.
|
||||
const rememberInfoScroll = (px: number) => void (infoScrollRef.current = px);
|
||||
|
||||
// The header search typeahead picked a title → open it the same way a card click would (a movie's
|
||||
// info page like the card menu's "Media info", a show's drill-down), then clear the hand-off so it
|
||||
// doesn't re-open.
|
||||
useEffect(() => {
|
||||
if (jumpTo != null) {
|
||||
if (jumpTo.type !== "show") infoScrollRef.current = 0; // a jumped-to info page starts at the top
|
||||
if (jumpTo.type !== "show") rememberInfoScroll(0); // a jumped-to info page starts at the top
|
||||
sub.open(
|
||||
jumpTo.type === "show"
|
||||
? { kind: "show", id: jumpTo.id }
|
||||
@@ -187,22 +197,6 @@ export default function PlexBrowse() {
|
||||
// restore it when we come back — so the browser/mouse Back from the player lands on the same card
|
||||
// instead of the top of the library. The scroller is App's <main> (the page's overflow-y-auto).
|
||||
const scrollRef = useRef(0);
|
||||
// Same idea for the info page: "Browse collection" (onFilter) leaves the info view for a filtered
|
||||
// grid, and browser Back returns to this same info page — restore where the user had scrolled it
|
||||
// (down to a collection strip) instead of snapping to the top. Reset on a fresh info open so a
|
||||
// different title's info starts at the top.
|
||||
const infoScrollRef = useRef(0);
|
||||
// ONE writer for that memory, so "stash on the way out of an info page, reset on a fresh one" is a
|
||||
// rule the code holds rather than one spread over four call sites — and so the compiler's
|
||||
// complaint about it is answered once, here, instead of at each of them.
|
||||
//
|
||||
// The complaint is a false positive of a conservative analysis, not a hazard: the rule objects to a
|
||||
// ref being written outside the effect that reads it, but that IS the arrangement a scroll memory
|
||||
// needs — the navigation handlers record where the page was, and the restore layout effect above
|
||||
// reads it on the way back. React's own guarantee (a ref write is never part of rendering) is what
|
||||
// makes it safe, and nothing here reads the value during render.
|
||||
// eslint-disable-next-line react-hooks/immutability -- a scroll memory: handlers record, the restore layout effect reads, nothing reads it in render
|
||||
const rememberInfoScroll = (px: number) => void (infoScrollRef.current = px);
|
||||
// One menu for the titles grid and one for the search's episodes (see ui/ContextMenu).
|
||||
const menu = useContextMenu<PlexCard>();
|
||||
const epMenu = useContextMenu<PlexCard>();
|
||||
|
||||
Reference in New Issue
Block a user