ESLint Plugin: react-hooks
This package provides two static checks: rules-of-hooks enforces where Hooks may be called, while exhaustive-deps checks dependency arrays for Hooks such as useEffect, useMemo, and useCallback.
It exists because Hook correctness depends on source-level structure: Hooks must execute in a consistent order, and reactive callbacks must account for values that can change between renders.
Sources: packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:304-306
Core concepts
Hook recognition
A Hook is recognized by a name equal to use or matching use[A-Z0-9]; member expressions are also accepted when the namespace begins with an uppercase letter.
Sources: packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:29-31, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:37-51
Component or Hook scope
A function is treated as component-or-Hook code when its name is component-like, Hook-like, or when it is a callback passed to forwardRef or memo.
Sources: packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:98-112
Reactive Hook
A reactive Hook is a built-in Hook such as useEffect, useLayoutEffect, useCallback, or useMemo, useImperativeHandle, or a configured custom Hook matched by additionalHooks.
Sources: packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1990-2033
Declared dependency
A declared dependency is represented by DeclaredDependency, which stores a dependency key and its AST node.
Sources: packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:26-29
Inferred dependency
An inferred dependency is represented by Dependency, which records whether the value is stable and all references that use it.
Sources: packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:31-34
Dependency tree
A DependencyTreeNode is a property-path tree whose nodes record used values, satisfied dependency declarations, deeper usage, and child properties.
Sources: packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:36-41
How rules-of-hooks detects conditional calls
The rule first records possible Hook calls by testing each call callee with isHook. It stores each Hook under the current code-path segment instead of deciding immediately, because the final answer depends on all paths through the function.
The rule identifies valid context by walking ancestors with isInsideComponentOrHook. It uses getFunctionName, isComponentName, and isHook, while also accepting forwardRef and memo callbacks.
At the end of a code path, onCodePathEnd evaluates whether every recorded Hook is reachable from the start and from every final segment. It computes paths with countPathsFromStart, countPathsToEnd, and shortestPathLengthToStart, caching results and marking cycles while traversing predecessor and successor segments.
This is how a condition or loop is detected statically: a Hook placed in only one branch does not have the same path coverage as a Hook that appears on every route, while a cycle is explicitly recorded rather than treated as an ordinary straight-line path.
Evidence
- hook-callpackages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:756
- hook-testpackages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:37
- path-analysispackages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:306
- path-countspackages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:338
- diagnosticpackages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:214
Sources: packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:756-799, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:98-112, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:304-306, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:338-388, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:412-455, packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts:479-512
How exhaustive-deps builds dependency recommendations
visitCallExpression classifies a call with getReactiveHookCallbackIndex. Built-in reactive Hooks map their callback to argument position zero, useImperativeHandle maps it to position one, and configured custom Hooks can match additionalHooks.
For an inline function callback, visitCallExpression calls visitFunctionWithDependencies. That function acquires the callback scope, finds the enclosing pure scopes, and treats variables declared there as values that may need to be listed.
gatherDependenciesRecursively walks references in the callback and child scopes. It ignores unresolved references and values outside pure scopes, finds the relevant AST node with fastFindReferenceWithParent, normalizes it with getDependency, and converts property access into a string path with analyzePropertyChain.
Property access is therefore preserved as a path such as props.foo.id, including optional-chain information. analyzePropertyChain recursively combines non-computed member names, while markNode records whether each path was optional or required. getDependency can widen a member expression to its containing object, except for .current, computed access, and certain call shapes.
Each dependency is stored with a stability decision. isStableKnownHookValue recognizes primitive constants and stable Hook results such as useRef, while isFunctionWithoutCapturedValues accepts a component-scope function when it captures no changing pure-scope value. The rule memoizes these decisions with memoizeWithWeakMap.
| Value category | Dependency treatment |
|---|---|
Primitive const literal | Considered stable |
useRef result | Considered stable |
Setter or dispatcher from useState, useReducer, or useActionState | Stable tuple value |
| Function without changing captures | Considered stable |
| Other render-scope value | Tracked as a dependency |
collectRecommendations compares inferred dependencies with declared dependencies by building a DependencyTreeNode hierarchy. It marks used paths, marks declared and stable paths as satisfied, and then scans the tree for missing and satisfying paths.
A declared parent can satisfy deeper property usage: the scan treats props.foo as sufficient for props.foo.id because the parent node is satisfied recursively. The rule formats optional paths with formatDependency, reports missing names through getWarningMessage, and can replace the dependency array with formatted suggestions.
Evidence
- hook-callpackages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1326
- callback-scopepackages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:181
- referencespackages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:505
- recommendationspackages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1535
- dependency-arraypackages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1314
The rule also warns about writes that occur inside a Hook callback because those assignments are lost after each render; reportStaleAssignment recommends moving the value into the Hook or storing mutable state in useRef. It can identify constructions such as objects, arrays, functions, classes, JSX, and regular expressions and offer wrapping fixes when those values cause unstable dependencies.
Sources: packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1326-1526, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1990-2033, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:181-1324, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:505-595, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1927-1968, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1901-1918, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1865-1893, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:266-403, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:406-461, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:163-177, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:576-589, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1535-1710, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1631-1663, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1021-1033, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1314-1320, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:643-658, packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts:1714-1763
How it connects
The package entry point imports ExhaustiveDeps, RulesOfHooks, and shared React Compiler rule configuration, so this lint package sits beside compiler-related lint integration rather than replacing it.
The separate react-compiler ESLint rule obtains a compiler result and turns compiler events into lint diagnostics. The supplied excerpts do not show eslint-plugin-react-hooks calling the React Compiler directly; they show why compiler integration exists: compiler validation also rejects conditional Hook calls, and compiler dependency validation infers dependencies for compilation.
Sources: packages/eslint-plugin-react-hooks/src/index.ts:7-16, compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts:175-182, compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts:102-114, compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts:495-519
Key takeaways
rules-of-hooksrecords Hook calls by code-path segment and compares reachability across branches and cycles.exhaustive-depsgathers resolved references from pure scopes, normalizes property paths, and classifies stable values.- A dependency tree lets a declared parent satisfy deeper property reads.
- The package is adjacent to compiler linting, but the shown code does not prove that these rules call the compiler directly.