refactor(frontend): the long tail — and eslint-suppressions.json is gone (E27 S2)
Closes the burn-down. Hook suppressions 113 -> 34, `exhaustive-deps` 45 -> 2 (the target was <10), and every one of the 34 survivors is an inline directive NAMING the invariant it protects. The suppressions file is deleted, because a bulk file hides the next violation instead of answering it — and it cannot hold a warning-level rule at all. The sprint plan's three verdicts missed the answer that turned out to be the most common one: adjust the state DURING RENDER. React re-runs the component and discards that pass rather than committing it, so the stale frame an effect-based reset paints does not exist. Applied to the Plex player's up-next card, the playlist view reset, the feed's optimistic overrides, the channel banner's retry count, the download center's tab guard, the Plex search highlight, the comfort-pin latch, Welcome's per-view outcome and the card pager's page clamp. It is legal only for a component's own state — PlexBrowse's language commit resets PlexProvider's filters, so that one stays an effect and says why. Mirrors of a prop or query became derived values or `draft ?? server`: - `downloadRow` had ui/Avatar's defect exactly — a boolean "poster failed" that an effect reset, in rows recycled down a virtualized list, so one job's missing poster painted over the next one's for a frame. Keyed by URL now. - Scheduler's two number editors held a mirror an effect re-synced, on a page that POLLS — the shape a poll can overwrite mid-edit. A draft while editing, the server's value otherwise; six tests where there were none, the one behavioural change (the save button stops being dirty at commit rather than at the echo) mutation-checked. - SetupWizard's "no token" and WatchPage's "gone" are facts about the URL, so they are seeded and derived rather than pushed into state by an effect that had to commit first. Elsewhere: PlexInfo's rating chip was a component declared during render — a new component type every render, so React remounted the subtree and dropped the focus on its filter button. Hoisted. Feed's undo action and PlexPlayer's session reclaim both re-entered their own useCallback, which the checker reads as a forward reference; both go through the ref idiom this file already uses. BootGate's dep list was load-bearing for correctness — a re-run meant `location.reload()` — so the "already adopted THIS identity" comparison now enforces it in code, with a test that fails without it (the existing reload tests all install their spy too late to catch it). One correction to the batch before this: keying ChatThread's and Messages' decrypt passes on `q.data` instead of `q.dataUpdatedAt` was wrong. react-query's structural sharing makes it safe in production, but the component must not melt down when something hands back a fresh object per render — and Messages.keyboard.dom.test does exactly that, so the effect span the suite. Both are back on the fetch as the trigger with the payload behind a ref, which is honest AND loop-proof; the replaced comment had warned about precisely this. CLAUDE.md gains four canonical-patterns rows for the above, and its "React 18" line is now true.
This commit is contained in:
@@ -188,13 +188,16 @@ export default tseslint.config(
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
// eslint-plugin-react-hooks 7's `recommended` is no longer two rules — it carries the whole
|
||||
// React Compiler rule set (E27 S1). On this codebase that lands 283 findings, and they were
|
||||
// React Compiler rule set (E27 S1). On this codebase that landed 283 findings, and they were
|
||||
// triaged rather than bulk-suppressed, because the two answers are different:
|
||||
//
|
||||
// react-hooks/refs 217 — OFF, see below.
|
||||
// the other 66 — real, and suppressed into eslint-suppressions.json as a RATCHET:
|
||||
// new violations fail the gate, the existing list burns down in E27 S2
|
||||
// (45 of them are `set-state-in-effect`, which IS that sprint's subject).
|
||||
// the other 66 — real. They went into eslint-suppressions.json as a ratchet for one
|
||||
// sprint and E27 S2 burned the file down to nothing: what survives is
|
||||
// an inline directive at the site, each naming the invariant it
|
||||
// protects. There is DELIBERATELY no suppressions file any more — a
|
||||
// bulk `--suppress-all` would hide the next violation instead of
|
||||
// answering it, and it cannot hold a warning-level rule anyway.
|
||||
//
|
||||
// `refs` is off because it fires on this app's popover architecture, not on its history, so
|
||||
// suppressing it would not help the next popover anyone writes. Measured over the 217:
|
||||
@@ -203,7 +206,7 @@ export default tseslint.config(
|
||||
// down as a prop (`popover={pop}`, `ref={fade.ref}`), which is what `components/ui`'s popover
|
||||
// engine is FOR. That is 153 of 217 flagged for having the word `refs` in an object that is not
|
||||
// a React ref. The remaining ~64 are genuine render-phase `someRef.current = x` writes and are
|
||||
// real; they are E27 S2's work, not this rule's, because the rule cannot tell them apart here.
|
||||
// real (E27 S2 answered the ones the other rules could see, at their sites).
|
||||
// Re-evaluate when the rule can: it shipped in 1.0.0 and this looks like over-approximation.
|
||||
"react-hooks/refs": "off",
|
||||
"local/jsx-comment-text": "error",
|
||||
|
||||
Reference in New Issue
Block a user