Skip to content

chore(lint): adopt the React Compiler rules, holding set-state-in-effect back - #177

Merged
tyler-rich merged 1 commit into
devfrom
claude/react-compiler-eslint-rules-y3mae8
Aug 9, 2026
Merged

chore(lint): adopt the React Compiler rules, holding set-state-in-effect back#177
tyler-rich merged 1 commit into
devfrom
claude/react-compiler-eslint-rules-y3mae8

Conversation

@tyler-rich

@tyler-rich tyler-rich commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Step 3 of the frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md — the step that
document flags as "a separate decision; may be declined" and prices as "unbounded until
measured"
. Adopted, with one rule held back.

No dependency version moves. frontend/package.json and frontend/package-lock.json are
byte-identical; the lockfile SHA-256 was captured before the baseline install and re-verified after
the final one. typescript stays at 5.7.2 (step 4).

The cost, measured before anything landed

Obtained with a throwaway probe config (a copy of eslint.config.js with the spread restored, run
via --config, then deleted), so the tracked tree was never modified to take the measurement.

Count Severity Rule
18 error react-hooks/set-state-in-effect
6 error react-hooks/refs
0 the other 12

Twelve of the fourteen rules report nothing — including immutability, purity and
preserve-manual-memoization, which the scoping doc's Step 3 row expected to fire in volume.

What this PR does

react-hooks/refs — 6 findings, fixed

All six are one idiom in ScansPage.tsx: a useRef(viewFromParams(searchParams)) whose .current
was read during render to seed six useState initializers. Replaced with a lazy useState
initializer, which runs viewFromParams exactly once on first render as the ref did, without the
render-time ref read. initialView had no other use in the file; useRef stays imported for
historyGuard.

react-hooks/set-state-in-effect — 18 findings, rule held back (#176)

Only 6 of the 18 are the synchronous setState the rule's rationale describes. #176 tracks
them, and names the two that are deliberate effects which each closed a real bug — ScanDetailPage's
:scanId reset (L17/P2-2) and ScansPage's compare reconcile (P3-2) — so a later
session doesn't "fix" them blind and re-open what they closed.

The other 12 are the fetch-on-mount idiom, where every setState runs after an await. These
are reported, not worked around, and are deliberately outside #176's scope. A three-shape probe
against the installed 7.1.1, with load defined in the component body:

Shape Reported?
void load() yes
void (async () => { await load(); })() no
load().catch(() => {}) yes

The first two are semantically identical, and moving the same load behind a custom hook silences
all three. So the report tracks what the compiler can see through, not a behavioural difference —
and the available "fixes" are a wrapper that changes nothing or a data-fetching refactor that hides
the call from the analyser. Neither is an improvement; if that refactor is ever worth doing it is
its own decision.

Verification

eslint --print-config before and after, on an app .tsx, a library .ts and a test override —
all three moved identically, 121 → 135 rules, with nothing else changed at any severity:

Severity Rules added
error (11) config, error-boundaries, gating, globals, immutability, preserve-manual-memoization, purity, refs, set-state-in-render, static-components, use-memo
warn (2) incompatible-library, unsupported-syntax
off (1) set-state-in-effect

Counts reconcile: configs.recommended is 16 rules, of which rules-of-hooks and exhaustive-deps
were already present from step 2, so the delta is exactly +14.

Suites, from a fresh rm -rf node_modules && npm ci: lint clean, format:check clean, 79 tests
across 21 files
, npm audit 0 vulnerabilities, build green.

The bundle moves, and should: index-BNB6IweX.js 645.18 kB → index-Vvdzytcz.js 645.14 kB.
Steps 1 and 2 were provably lint-only with identical asset hashes; this step changes runtime code in
ScansPage.tsx, so the new hash is the honest signal that it did. CSS untouched
(index-D2wHtcHV.css).

Not done

No --fix, in bulk or individually. No rule disabled beyond the single authorised override. No step
4 work. docs/upgrades/frontend-toolchain-86.md and docs/ROADMAP.md not edited — correcting the
sequence document is a maintainer call.

See docs/ARCHIVE.md § Deviations for changes made in this step.

…ect back

Step 3 of the frontend toolchain sweep in
docs/upgrades/frontend-toolchain-86.md. No dependency version moves:
package.json and package-lock.json are byte-identical. Step 2's holding
edit is replaced by the ...reactHooks.configs.recommended.rules spread it
was standing in for, taking the resolved config 121 -> 135 rules across all
three file classes.

Twelve of the fourteen added rules report nothing against the current tree,
including immutability, purity and preserve-manual-memoization. The cost is
two rules and 24 findings.

react-hooks/refs (6 findings) is fixed by hand: all six are one idiom in
ScansPage, a useRef whose .current was read during render to seed six
useState initializers. A lazy useState initializer runs viewFromParams once
on first render exactly as the ref did, without the render-time ref read.

react-hooks/set-state-in-effect (18 findings) is left off, with the reason
in the config and the work tracked in #176. Only 6 of the 18 are the
synchronous setState the rule's rationale describes, and two of those are
deliberate effects that each closed a real bug (L17/P2-2 and P3-2), so #176
names them so they are not "fixed" blind. The other 12 are the
fetch-on-mount idiom where every setState runs after an await; a probe
showed the report tracks what the compiler can see rather than a
behavioural difference, so they are reported rather than worked around and
are deliberately outside #176's scope.

See docs/ARCHIVE.md § Deviations for changes made in this step.
@tyler-rich
tyler-rich merged commit 6902036 into dev Aug 9, 2026
8 checks passed
tyler-rich added a commit that referenced this pull request Aug 9, 2026
…r 8 (#189)

Three Track A edits in docs/ROADMAP.md, each gated on a live check made
this session rather than on a prior session's claim:

- Strike the "Frontend tooling majors from Dependabot #86" item as Done
  2026-08-09, naming all eight sweep PRs (#171, #174, #177, #179, #180,
  #183, #185, #187) and both deliberately-excluded packages (TypeScript 7
  and @types/node 26). All eight squash commits re-confirmed present on
  dev before writing the bullet.

- Strike the GHSA-qwww-vcr4-c8h2 re-cut request: the advisory has already
  been re-cut upstream to >= 7.12.0, < 7.18.2 plus >= 8.0.0, < 8.3.0,
  with the 8.x range untouched. Verified against the record in
  github/advisory-database and independently against the npm registry's
  advisory endpoint, which returns nothing for 7.18.2.

- Reword react-router 7 -> 8 as blocked on a React 19 decision rather
  than on a tooling bump, and move it out of the tooling-majors grouping
  into Longer-term / speculative. react-router@8.3.0 peers react and
  react-dom at >=19.2.7; this repo pins both at 18.3.1.

See docs/ARCHIVE.md section 14 for the dated record of what was verified
for each.
tyler-rich added a commit that referenced this pull request Aug 9, 2026
…a current reason (#193)

Comment-only change to the npm /frontend entry. No ignore rule, group,
schedule, target-branch or any other key is added, removed or modified.

The paragraph's instruction — leave the frontend tooling majors unignored
so Dependabot keeps surfacing them — is still correct. Its stated reason
was not: it called them "the deferred #86 sweep" and said to surface them
"until that PR is done". The sweep completed 2026-08-09 across #171,
#174, #177, #179, #180, #183, #185 and #187, and docs/ROADMAP.md marks
the item Done (verified before writing). So the comment had begun
arguing against its own instruction — a reader who checked the roadmap
would find the work finished and reasonably conclude the exemption had
expired, whose obvious next move is to add the typescript ignore, which
is the one thing that must not happen.

Rewritten so the instruction rests on two current reasons. The tooling
majors stay unignored as ordinary policy: we want to see them, evaluate
them and land them, which is exactly what the sweep did (pointing at
docs/upgrades/frontend-toolchain-86.md rather than restating it).
typescript stays unignored for a sharper reason: TypeScript 7 is wanted,
the blocker is entirely upstream, and the regenerating Dependabot PR
proposing 7.x IS the notification that tells us when typescript-eslint
ships support — an ignore would suppress the signal while changing
nothing about the blocker. The re-check command is inlined so the next
reader can test it without a session, along with the expected shape of
support (a new typescript-eslint MAJOR built against TS 7's ./unstable/*
API, not a point-release range widen).

Verified by parsing rather than by reading: both versions load with
yaml.safe_load to byte-identical canonical JSON (SHA-256 07e71c6c on each
side), and every changed raw line matches ^[+-]\s*#.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant