chore(frontend): adopt the React Compiler lint rules from eslint-plugin-react-hooks v7 (#182) #540

Merged
claude-bot merged 3 commits from chore/182-react-compiler-lint into main 2026-09-06 21:00:36 +00:00
Contributor

Closes #182. Second engineering lane of v4.4.0 (#535 phase 0): the rules go on before the rebuild writes new components.

Re-measured first. The July probe found 12 sites; against today's main the full preset finds 40 across 22 files (34 set-state-in-effect, 2 purity, 1 refs, 1 immutability, 1 preserve-manual-memoization, 1 exhaustive-deps warning). Two facts about the rule shaped the fixes: it reports one site per effect, so every synchronous write in a flagged effect had to go; and it does not treat await as deferring, so an async helper called from an effect is flagged even when every write follows an await.

The fix, applied uniformly. About 24 of the 34 were one shape: a loading / stale / "unknown" flag an effect raised on its way past. Each now stores what the data in hand was fetched for and derives the flag during render (loading = loadedFor !== key). The rest: ElapsedTimer reads the clock once in a state initialiser instead of during render; GeneratorPanel syncs its ref in an effect; SessionShelf's pointer listeners come off through one AbortController per gesture instead of a handler removing itself by a not-yet-initialised name; WikiArticle's memo gets a dependency the compiler can verify. No eslint-disable anywhere; two pre-existing ones became unnecessary and were removed. No test file was modified.

One user-visible change, deliberate. WikiDraftReview's stat-block autosave indicator set "Saved" and then wiped it on the next render, so the autosave looked like it had done nothing. Deriving the indicator makes "Saved" stick until the next edit. It has its own **Fixed** entry. The body autosave indicator in the same file has the same defect, was not flagged (the rule bails on it), and needs a real refactor of the autosave path: filed as #539 for the wiki lane of the rebuild.

Config. recommended-latest rather than recommended: in v7 both are flat configs of the same shape and the former is a strict superset (adds void-use-memo); the source is clean under both. exhaustive-deps restated at warn. The comment no longer claims the preset is excluded and says the compiler-on-the-build question is a separate decision this change does not make.

Verification (Docker, node 22): eslint 0 errors, 0 warnings; 64 files, 799 tests pass (unchanged); build ok; the 23 touched-file test files run twice more with no flakes. A byte scan of every changed file confirmed no stray NUL bytes (an editor artefact the lane caught and fixed).

Small behavioural improvements beyond the headline, each noted in the code: an aborted read no longer marks a page "loaded" (JoinCampaign, WikiArticle), a stale search error no longer shows while a newer term is in flight (WikiSearch), the proposals badge no longer blanks during a re-read (CampaignStoryline), and a retry keeps the old error on screen until the retry lands rather than flickering (SessionDetail).

🤖 Generated with Claude Code

Closes #182. Second engineering lane of v4.4.0 (#535 phase 0): the rules go on before the rebuild writes new components. **Re-measured first.** The July probe found 12 sites; against today's main the full preset finds **40 across 22 files** (34 `set-state-in-effect`, 2 `purity`, 1 `refs`, 1 `immutability`, 1 `preserve-manual-memoization`, 1 `exhaustive-deps` warning). Two facts about the rule shaped the fixes: it reports one site per effect, so every synchronous write in a flagged effect had to go; and it does not treat `await` as deferring, so an `async` helper called from an effect is flagged even when every write follows an `await`. **The fix, applied uniformly.** About 24 of the 34 were one shape: a `loading` / `stale` / "unknown" flag an effect raised on its way past. Each now stores *what the data in hand was fetched for* and derives the flag during render (`loading = loadedFor !== key`). The rest: `ElapsedTimer` reads the clock once in a state initialiser instead of during render; `GeneratorPanel` syncs its ref in an effect; `SessionShelf`'s pointer listeners come off through one `AbortController` per gesture instead of a handler removing itself by a not-yet-initialised name; `WikiArticle`'s memo gets a dependency the compiler can verify. **No `eslint-disable` anywhere**; two pre-existing ones became unnecessary and were removed. No test file was modified. **One user-visible change, deliberate.** `WikiDraftReview`'s stat-block autosave indicator set "Saved" and then wiped it on the next render, so the autosave looked like it had done nothing. Deriving the indicator makes "Saved" stick until the next edit. It has its own `**Fixed**` entry. The body autosave indicator in the same file has the same defect, was not flagged (the rule bails on it), and needs a real refactor of the autosave path: filed as #539 for the wiki lane of the rebuild. **Config.** `recommended-latest` rather than `recommended`: in v7 both are flat configs of the same shape and the former is a strict superset (adds `void-use-memo`); the source is clean under both. `exhaustive-deps` restated at `warn`. The comment no longer claims the preset is excluded and says the compiler-on-the-build question is a separate decision this change does not make. **Verification** (Docker, node 22): eslint 0 errors, 0 warnings; 64 files, 799 tests pass (unchanged); build ok; the 23 touched-file test files run twice more with no flakes. A byte scan of every changed file confirmed no stray NUL bytes (an editor artefact the lane caught and fixed). Small behavioural improvements beyond the headline, each noted in the code: an aborted read no longer marks a page "loaded" (`JoinCampaign`, `WikiArticle`), a stale search error no longer shows while a newer term is in flight (`WikiSearch`), the proposals badge no longer blanks during a re-read (`CampaignStoryline`), and a retry keeps the old error on screen until the retry lands rather than flickering (`SessionDetail`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chore(frontend): adopt the React Compiler lint rules from eslint-plugin-react-hooks v7 (#182)
Some checks failed
CI / Bot/backend version sync (pull_request) Successful in 54s
CI / Backend lint (ruff) (pull_request) Successful in 1m0s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m27s
CI / Bot tests and audit (pull_request) Successful in 2m34s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m38s
CI / Docker image build (pull_request) Successful in 4m6s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m25s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
db7dee1f17
The config enabled `rules-of-hooks` and `exhaustive-deps` by hand and
deliberately skipped the plugin's preset, which in v7 also carries the
React Compiler rule set. The preset (`recommended-latest`) is now on in
full, and the 40 violations it found across 22 files are fixed rather
than suppressed — no `eslint-disable` comments were added.

Most of them were the same shape: a `loading`, `stale` or "unknown" flag
that a fetch effect raised on its way past, rewritten as a value derived
during render from what the data in hand was fetched for.

Sites fixed:
- ElapsedTimer: read the clock in a state initialiser, not a useMemo, and drop the redundant tick at effect start (purity x2, set-state-in-effect)
- BeatNotesEditor: derive `loading` and `stale` from "what is shown vs what was asked for"; key "Saved." to the session (set-state-in-effect x2)
- CampaignAiSettings: derive `loading` from the campaign the settings belong to
- ColdOpenPanel: key the panel body by session so the stored flags are read once, at mount
- GeneratorPanel: keep `valuesRef` in step from an effect, not during render (refs)
- LorePicker: derive `loading` from the search the results answer
- SeriesManager: drop a `setLoading(false)` nothing ever read
- ShelfPalette: derive the wiki hit list and the keyboard highlight (set-state-in-effect x2)
- VotingGrid: derive the empty conflict overlay from the slots on screen
- WikiSearch: tag the finished search with its term; "searching" is then a comparison
- workbenchTools: tag the beat-notes probe with the session it answered for
- Admin: promise-chain `refreshCapabilities`; leave `loading` true for the first read in the recordings and backup sections; derive it in the audit log (set-state-in-effect x4)
- CampaignDetail: guard on the linked system id the effect depends on (exhaustive-deps)
- CampaignHighlights: derive `loading` from the quotes query
- CampaignPlanning: default both session pickers during render (set-state-in-effect x2)
- CampaignStoryline: derive `loading`; clear the proposals error on the read that worked (set-state-in-effect x2)
- JoinCampaign: derive `loading` from the invite code
- SessionDetail: derive the summary-run panel from the session; clear the loot and proposal errors on success (set-state-in-effect x3)
- SessionShelf: take the drag listeners off through an AbortSignal instead of a self-reference (immutability)
- WikiArticle: derive `loading`, the stat schema, the owner list, the pending draft and the link-check note; hoist `can_view_private_notes` out of a useCallback (set-state-in-effect x5, preserve-manual-memoization)
- WikiDraftReview: derive `loading` and the stat autosave indicator (set-state-in-effect x2)
- WikiNewEntry: tag the link-check note with the body it was produced for
- WikiProposals: derive `loading` from the campaign

One user-visible change falls out, and it is an improvement: the stat
block's "Saved" indicator now stays until the next edit instead of being
wiped on the render after the autosave landed. It has its own Fixed entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rbrooks force-pushed chore/182-react-compiler-lint from db7dee1f17
Some checks failed
CI / Bot/backend version sync (pull_request) Successful in 54s
CI / Backend lint (ruff) (pull_request) Successful in 1m0s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m27s
CI / Bot tests and audit (pull_request) Successful in 2m34s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m38s
CI / Docker image build (pull_request) Successful in 4m6s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m25s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
to 90a36b2e45
Some checks failed
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Bot/backend version sync (pull_request) Successful in 48s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m24s
CI / Bot tests and audit (pull_request) Successful in 1m44s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m49s
CI / Docker image build (pull_request) Successful in 4m11s
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
2026-09-06 20:37:00 +00:00
Compare
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-06 20:41:25 +00:00
rbrooks force-pushed chore/182-react-compiler-lint from 90a36b2e45
Some checks failed
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Bot/backend version sync (pull_request) Successful in 48s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m24s
CI / Bot tests and audit (pull_request) Successful in 1m44s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m49s
CI / Docker image build (pull_request) Successful in 4m11s
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
to ebc3154c5f
Some checks failed
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Has been cancelled
CI / Bot/backend version sync (pull_request) Has been cancelled
CI / Bot tests and audit (pull_request) Has been cancelled
CI / Backend lint (ruff) (pull_request) Has been cancelled
CI / Docker image build (pull_request) Has been cancelled
2026-09-06 20:41:55 +00:00
Compare
docs(changelog): file the stat-block autosave fix under Fixed, not Internal (#182)
All checks were successful
CI / Docker image build (pull_request) Successful in 42s
CI / Bot/backend version sync (pull_request) Successful in 45s
CI / Backend lint (ruff) (pull_request) Successful in 49s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m9s
CI / Bot tests and audit (pull_request) Successful in 1m49s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m25s
CI / Backend migration, tests, and audit (pull_request) Successful in 7m16s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 18m1s
a3171f1add
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude-bot deleted branch chore/182-react-compiler-lint 2026-09-06 21:00:36 +00:00
Sign in to join this conversation.
No description provided.