Skip to content

chore: wire up eslint with the React hooks rules - #81

Closed
matej21 wants to merge 1 commit into
mainfrom
chore/eslint-react-hooks
Closed

chore: wire up eslint with the React hooks rules#81
matej21 wants to merge 1 commit into
mainfrom
chore/eslint-react-hooks

Conversation

@matej21

@matej21 matej21 commented Aug 20, 2026

Copy link
Copy Markdown
Member

Why

The repo has no eslint config. None — no .eslintrc*, no eslint.config.*. Yet the source is sprinkled with // eslint-disable-next-line react-hooks/rules-of-hooks. Every one of those comments is decoration.

That is not theoretical. One sat over a useField call inside a for loop in <Switch>, under a comment asserting a "stable count, stable order" that nothing enforced and that was false. It crashes React with Rendered more hooks than during the previous render the moment a <Case> is conditionally rendered (fixed separately in #78). It shipped because no linter existed to contradict the comment.

What this adds

eslint + eslint-plugin-react-hooks + @typescript-eslint/parser, a flat config, and a lint script. Two rules only:

rule level
react-hooks/rules-of-hooks error
react-hooks/exhaustive-deps warn

eslint-plugin-react-hooks v7 ships 28 rules (the React Compiler set — purity, set-state-in-effect, immutability, …). The plugin is registered but none of the others are enabled, and no style preset is added. A repo with no lint history does not need a thousand formatting complaints burying the signal.

No CI job is added, deliberately — a gate that fails on day one would block every open PR. The step is written out at the bottom for when it is ready.

What it finds

Inline disables suppress reports, so there are two numbers:

invocation result
bun run lint 28 errors, 17 warnings
eslint . --no-inline-config 9 errors, 17 warnings

The 9 rules-of-hooks sites

file:line read
bindx-react/src/jsx/components/Switch.tsx:147 real crash — the known one, fixed in #78
bindx-react/src/jsx/components/Field.tsx:31 real, unsuppresseduseField after if (field == null) return null. The code's own comment names the toggling condition: "when accessing field on disconnected has-one relation"
bindx-react/src/jsx/components/Attribute.tsx:41 real, unsuppressed — same shape
bindx-ui/src/datagrid/filters/common.tsx:11 realname ??= useDataViewFilterName(); ??= short-circuits, so the hook runs only when the prop is nullish
bindx-ui/src/datagrid/filters/mobile.tsx:11 real — identical ??=
bindx-ui/src/datagrid/column-header.tsx:39 realuseDataViewFilter(filterName) inside if (filterName); toggling a column filter changes the hook count
bindx-dataview/src/filterComponents.tsx:68 realuseOptionalDataViewFilterName() inside a plain function called from ~16 component bodies, and only when name === undefined. Not use-prefixed and conditional
bindx-react/src/jsx/componentFactory.ts:81 false positive — the loop is over a closure constant built outside the component
bindx-react/src/jsx/componentFactory.ts:150 false positive — same reason

7 genuine hazards, 2 false positives. Note the inversion: the two sites carrying reassuring comments are the false positives; the six carrying bare disables are all real.

The caveat you will hit immediately

25 of the 28 default-run errors are not hooks findings — they are Definition for rule … was not found on existing @typescript-eslint/* disable comments (17 × no-explicit-any, 6 × ban-types, 1 × no-empty-interface, 1 × no-unused-vars). ESLint errors on any disable directive naming an undefined rule and offers no knob to downgrade it.

This is the same disease, one plugin over: those comments are decoration too.

Installing @typescript-eslint/eslint-plugin to define the namespace was tried and rejected — it converts 19 phantom errors into 19 Unused eslint-disable directive warnings and still leaves 6, because ban-types was deleted in typescript-eslint v8. A dependency that buys nothing. The two real options are to delete the 25 dead directives or to accept the noise; both are follow-ups.

Before the CI gate can go on

bun run lint must exit 0. Blocking it today: the 25 dead directives, plus 3 real rules-of-hooks errors that are unsuppressed (componentFactory.ts:150, Field.tsx:31, Attribute.tsx:41) — two of which are genuine crash hazards. The other 6 stay hidden behind their disables and can be worked through afterwards.

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile
      - run: bun run lint

Gates

bun run typecheck exit 0 · bun test --path-ignore-patterns='**/tests/browser/**' 1737 pass / 0 fail / 149 files · no source file modified.

Not done

No finding is fixed and no existing eslint-disable is removed — several become load-bearing once the linter is real, and each is its own decision. Extending the files glob to packages/example (sources sit at the package root, not src/) was probed: 23 files, 0 problems. Free to add.

The repo had no eslint config at all, while the source carried
`// eslint-disable-next-line react-hooks/rules-of-hooks` comments that
nothing has ever enforced. One of them sat over a `useField` call inside
a loop in <Switch>, under a comment asserting a "stable count" that was
false; it crashes React as soon as a <Case> is conditionally rendered.

Enable exactly two rules over packages/*/src and tests: rules-of-hooks as
an error, exhaustive-deps as a warning. eslint-plugin-react-hooks v7
ships 28 rules (the React Compiler set); none of the others are turned on
and no style preset is added, so the signal stays readable in a repo with
no lint history.

No CI job is added. `bun run lint` exits 1 today: 9 rules-of-hooks sites
(7 genuine hazards, 2 provably-stable false positives) and 25 phantom
errors from dead `@typescript-eslint/*` disable directives naming rules
no config defines. Turning the gate on has to wait for those.

Flat config is named .mjs because the root package.json has no
"type": "module".
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