Skip to content

fix(react): derive state instead of effect sync - #40

Merged
htcom-code merged 1 commit into
mainfrom
fix/react-effect-lint-warnings
Aug 24, 2026
Merged

htcom-code merged 1 commit into
mainfrom
fix/react-effect-lint-warnings

Conversation

@htcom-code

Copy link
Copy Markdown
Owner

oxlint 1.79.0 (merged in #39) expanded its react plugin and surfaced 6 new warnings in existing code. Three were real, one was dead code, and two had no action behind them. Lint is back to the 3 pre-existing only-export-components warnings.

Not a cosmetic pass: use-trace-store.ts is the file that produced the cold-start render loop fixed in #27, so its warnings were treated as signal.

What each warning turned out to be

Site Rule Verdict
module-detail-panel.tsx:32 set-state-in-effect Real — stale frame on module switch
use-trace-store.ts:63 set-state-in-effect Real — state sync that can be a derivation
use-trace-store.ts:54 refs Real — ref written during render
use-persistent-state.ts:35 refs Dead code — the ref was never read
trace-table.tsx:73, module-table.tsx:76 incompatible-library No action — React Compiler advice, compiler not in use

The three fixes

module-detail-panel — routes were reset from the effect. On a moduleId change the effect ran after commit, so one frame rendered the new module's header next to the previous module's routes. The fetched result now carries the module it belongs to and the view derives from it, so a mismatch means "loading" with no stale frame possible. This is the pattern the brain records as "비동기 결과를 입력과 함께 저장해 파생시켜라".

use-trace-store — sample mode is now derived, not synced. Sample mode is a pure function of the traces handed in, so it is computed during render via useMemo instead of pushed into state by an effect. That deletes the setRows → render → effect cycle that an unstable liveTraces identity used to drive (the "Maximum update depth exceeded" bug from #27) and the sameRows guard that existed only to break that cycle — the defect class is now structurally impossible rather than guarded against. Sample-mode clear keeps its exact old semantics by recording which batch was dismissed (same store-the-input idiom); loadOlder becomes a no-op in sample mode, where hasMore was already always false.

use-trace-store — the latest-rows ref is mirrored in an effect rather than assigned during render. loadOlder reads it from an event handler, which always runs after effects have flushed.

use-persistent-state — dead ref removed. Its comment claimed the ref kept the setter identity stable, but the setter is a useCallback([]) using the functional update form, and the ref was never read.

The two left alone

react/incompatible-library fires on useVirtualizer because it returns functions the React Compiler cannot memoize. This project does not run the compiler (plain @vitejs/plugin-react, no babel-plugin-react-compiler), so the warning has no fix — only suppression — and it would fire on every future virtualized table. It is turned off in .oxlintrc.json with the condition to re-enable written next to it.

Verification

  • npm run lint — exit 0, 9 warnings → 3 (the pre-existing only-export-components in src/components/ui/)
  • npm run build / tsc -b — exit 0
  • Built CSS is 30 bytes smaller than main and the only difference is one dropped rule: .contents{display:contents}. That utility existed solely because the word "contents" appeared in a source comment that this PR removes — nothing uses class="contents" (grepped). Not a regression.
  • Browser check (no test suite in this repo, so the changed paths were driven by hand against the dev server in SAMPLE DATA mode):
    • cold start renders 60 rows; console has no React warnings — in particular no "Maximum update depth exceeded" — only the expected proxy 502s with no platform on :8080
    • module switch with the panel open: the first committed frame after the click already reads Module wk-crash + ROUTES loading…, never the prior module's routes
    • Clear (two-click confirm) empties the sample view and it stays empty, matching the previous behaviour
    • reload repeats the clean cold start

- oxlint 1.79.0's react plugin flagged four sites: three were real, one
  was dead code
- module-detail-panel: the routes effect reset state when `moduleId`
  changed, so the previous module's routes stayed committed for one
  frame. The result now carries the module it belongs to and the view is
  derived from it, so a mismatch reads as "loading"
- use-trace-store: sample mode is a pure function of the traces handed
  in, so it is derived during render. That removes the setRows -> render
  -> effect cycle an unstable `liveTraces` identity used to drive (#27)
  and the `sameRows` guard that existed only to break it
- use-trace-store: the latest-rows ref is mirrored in an effect rather
  than written during render
- use-persistent-state: the latest-value ref was never read (the setter
  is already stable through its functional update), so it is gone

chore(lint): drop non-actionable virtualizer rule

- react/incompatible-library reports `useVirtualizer` returning
  functions the React Compiler cannot memoize. We do not run the
  compiler, so the two warnings had nothing behind them; the config
  records the condition to re-enable

Tags: #lint #react
Co-Authored-By: htjulia <htjulia1@gmail.com>
@htcom-code
htcom-code merged commit 0e81071 into main Aug 24, 2026
2 checks passed
@htcom-code
htcom-code deleted the fix/react-effect-lint-warnings branch August 24, 2026 01:23
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