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:
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:
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-depsexhaustive-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:
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 renderEvery 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.