7e1aeeb9fef4529e472e80974b7099c5d35a0f5d
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7e1aeeb9fe |
refactor(frontend): honest effect deps in the app shell and the ui layer (E27 S2)
Burns the first 14 of the 113 hook suppressions. Three verdicts, applied per site:
A useEffect that should not exist (6):
- Avatar remembered "the image failed" as a boolean an effect reset on src change. The
component is recycled across items in the virtualized feed, so after one avatar 429s the
next item's slot committed the fallback initial over a perfectly good image and only the
post-paint effect cleared it. The failure is now keyed BY URL; derived state cannot be stale.
- Combobox cleared the typed query from an effect on `open`. Moved into setOpenAnd, the single
funnel every close goes through. That also stops a spurious onQueryChange("") AT MOUNT, which
told a controlled caller its own initial query had been cleared before the user touched it.
- CommandPalette reset query + picked row from an effect. Closed now means UNMOUNTED, not
hidden, so a fresh open starts blank because mounting already means that -- and the ranking
memo over every channel, playlist and saved view stops running while the palette is shut.
- Pager's page box held a copy of the page that an effect re-synced; it now derives it and
keeps a draft only while being edited.
- App's ?link= / ?oauth= / ?admin= one-shots asserted "once per mount" with an empty dep list.
The ref guard the codebase already uses in 22 places ENFORCES it, so the honest deps can
include `t` -- which a language switch legitimately changes, and which would otherwise have
re-fired the toast.
Honest deps that also fix something (5):
- App's module preload was keyed on the account id, so a permission change that alters the
module set without moving the id never re-preloaded. `preload()` is idempotent, so depending
on `me` costs nothing.
- App's access-request nudge now re-issues on a language switch; it used to stay in the old
language until the pending count changed.
- App's diagnostics effect: applyDiagnostics already no-ops on an unchanged effective level,
so picking four fields out by hand was guarding nothing and missed any field added later.
- DataTable's two filter effects: a ref holds the latest onFiltersChange so commitFilters can
be stable, which is what lets the effects name it instead of switching the check off.
Genuine escape hatches (7) moved OUT of eslint-suppressions.json into inline directives that
name the invariant at the site -- the toast handover's render-phase global, the live region's
deliberate arming tick, the docked one-slot component, the popover engine's floating-ui list
state, dnd-kit's per-drag `moved` flag, BackToTop's DOM read and the palette's debounced
announcement. A reader of the code now sees why; a JSON file no one opens does not.
Tests: Avatar (3, new file) and Pager (3) pin the derived-state frames with a layout-phase
probe -- layout effects run before any passive effect, so they read the frame the browser would
paint. Combobox (2) pins the close-clears-query contract and the no-callback-at-mount rule.
All three sets mutation-checked against the effect-based originals.
Suppressions 113 -> 99; exhaustive-deps 45 -> 35.
|
||
|
|
7d682c90d5 |
build(frontend): React 18 -> 19 with the React Compiler (E27 S1)
The compiler auto-memoises, which is the point: it infers what the app's 112 `useCallback` and 48 `useMemo` call sites were hand-written for and applies the equivalent itself. It is on repo-wide with no opt-out list — it bails on anything it cannot prove safe, and says so through the lint rules. Verified in the built bundle rather than assumed: 369 `react.memo_cache_sentinel` markers across 35 of the 76 chunks. ONE transform, shared by vite.config.ts and vitest.config.ts through reactPlugin.ts. Not two `react()` calls: if the compiler ran only in the production build, every vitest run would exercise code the user never receives and a compiler-induced change would ship green — the same gate-vs-image split 0.93.2 was released to fix. The plugin is imported rather than named by the usual string, because a string is invisible to knip and, worse, a typo in it is a silent no-op that ships an uncompiled bundle, while a bad import is a tsc error. eslint-plugin-react-hooks 5 -> 7 brings the whole compiler rule set: 283 findings, triaged rather than bulk-suppressed, because the two halves needed different answers. react-hooks/refs (217) is OFF. 153 of those 217 are this app's popover architecture, not its history: 79 are @floating-ui/react's documented `pop.refs.setReference` / `getReferenceProps` callback-ref setters, and 74 more are passing the popover handle down as a prop, which is what components/ui's popover engine is for. Suppressing them would not help the next popover anyone writes. The remaining ~64 are genuine render-phase `ref.current = x` writes and are real, but the rule cannot tell them apart here; they are E27 S2's work. The other 66 go into eslint-suppressions.json as a ratchet — new violations fail the gate, and `--prune-suppressions` makes a fixed one fail too until it is removed. 45 of them are `set-state-in-effect`, which IS E27 S2's subject, so that sprint now has a measured worklist. react-i18next deliberately stays on 14. It never declared a React-19 peer — every version from 14 to 17 asks only for `react >= 16.8.0` — and what the newer lines actually require is i18next 25/26 against our 23. That is an i18n runtime migration wearing a React hat, and it buys this sprint nothing. Two React 19 type changes: `useRef<T>(null)` is now `RefObject<T | null>` (the honest shape — the element is null until it mounts), so PlexPlayerSettings' prop follows; and reactPlugin.ts joins tsconfig.node.json's include. The compiler skips memoising VirtualGrid and reports it: TanStack Virtual's `useVirtualizer()` returns functions that cannot be memoised without going stale. That is the right trade for a component whose cost is its row window, and it is the one such site in the app, so it carries a targeted disable with the reason — a second incompatible library anywhere else must still stop the gate. Nine lanes green, vitest 1558. Browser UAT on the dev server against the still-running React 18 build as a control: boot, feed (virtualized grid, popover menu, a filter change that moved the list 195,187 -> 64 items with the URL mirrored, so no stale memoised UI), command palette, player modal — no console errors on any of it. |