Linting Hooks

The cheapest tests in a React 7,897 project are not tests. eslint-plugin-react-hooks 7.1.1 reports violations no runtime test would catch, because the broken cases appear only on a path you did not exercise. Install it with npm 2,036 i -D eslint eslint-plugin-react-hooks and spread its recommended flat config:

eslint.config.jsJavaScript
import reactHooks from 'eslint-plugin-react-hooks';
export default [
  reactHooks.configs.flat.recommended,
  { files: ['**/*.{js,jsx}'],
    languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
];

The plugin identifies hooks by name — any function starting with use and a capital letter — and requires that they be called only from a PascalCase function or another useSomething, unconditionally, in the same order on every render (Controlled Inputs explains why). Point it at a useState inside an if and an effect that reads a prop it never declared:

Output of 129
   7:21  error    React Hook "useState" is called conditionally. React Hooks must be
                  called in the exact same order in every render  rules-of-hooks
  15:6   warning  React Hook useEffect has a missing dependency: 'id'. Either include
                  it or remove the dependency array             exhaustive-deps

exhaustive-deps is the rule people disable. Resist: an effect whose array omits a value it reads keeps using the value from the render in which it last ran — exactly the stale fetch above.

Version 7 is a bigger tool than its name suggests. Since React Compiler reached 1.0 in October 2025, the recommended preset also enables rules built on the compiler's analysis — set-state-in-effect, purity, refs, immutability and a dozen more — which check the Rules of React, not only the Rules of Hooks. The compiler itself need not be installed:

Output of 129
   8:5   error  Calling setState synchronously within an effect can trigger
                cascading renders
>  8 |     setCount(items.length);
     |     ^^^^^^^^ Avoid calling setState() directly within an effect
  12:14  error  Cannot call impure function during render
> 12 |   const id = Math.random();
  ...
  14:47  error  Cannot access refs during render

Every one is a real defect: the effect forces a second render pass, Math.random() gives the element a new id each render, and reading ref.current during render returns whatever the previous commit left behind (Pure Rendering and Effects You Do Not Need). Run the linter beside the tests — "verify": "eslint . && vitest run" — so a pull request cannot be green while breaking the rules React depends on. Adopt them as warnings first on an existing codebase; enabling seventeen at once produces a wall nobody reads.