hooks-exhaustive-deps
hooks-exhaustive-deps rule
Enforce exhaustive dependencies in React hooks to prevent stale closures. This rule is part of eslint-plugin-react-features and provides LLM-optimized error messages with suggestions.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Warning (correctness) |
| Auto-Fix | ๐ก Suggests fixes |
| Category | React |
| ESLint MCP | โ Optimized for ESLint MCP integration |
| Best For | All React projects using hooks |
Rule Details
Why This Matters
| Issue | Impact | Solution |
|---|---|---|
| ๐ Stale Closures | Outdated values in callbacks | Add all reactive dependencies |
| ๐ Missing Updates | Effect doesn't re-run | Include all used variables |
| ๐ Infinite Loops | Effect triggers itself | Memoize object/function deps |
| โก Performance Issues | Unnecessary effect runs | Remove constant dependencies |
Hooks Covered
| Hook | Description | Dependency Array Purpose |
|---|---|---|
useEffect | Side effects | When to re-run effect |
useLayoutEffect | Synchronous DOM updates | When to re-run effect |
useCallback | Memoize functions | When to recreate function |
useMemo | Memoize values | When to recalculate value |
useImperativeHandle | Customize ref handle | When to recreate handle |
Examples
โ Incorrect
// Missing dependency
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchResults(query).then(setResults);
}, []); // โ Missing 'query' dependency
// Effect won't re-run when query changes!
return <ResultList items={results} />;
}
// Missing function dependency
function UserProfile({ userId }) {
const loadUser = () => fetch(`/api/users/${userId}`);
useEffect(() => {
loadUser().then(setUser);
}, []); // โ Missing 'loadUser' and 'userId'
// ...
}โ Correct
import { useEffect, useCallback, useRef, useState } from 'react';
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const latest = useRef(query);
useEffect(() => {
// setResults (a useState setter) and latest (a useRef) are stable: not dependencies
latest.current = query;
fetchResults(query).then(setResults);
}, [query]);
// A dependency listed as a member path covers the callback reading it
const label = useCallback(() => `Results for ${query}`, [query]);
return <ResultList items={results} label={label()} />;
}What counts
- The hook is React's, by binding:
useEffect, a renamed import (import { useEffect as useFx }),React.useEffect,React['useEffect']or a namespace import. A local function, or an import from another module, that is calleduseEffectis not checked.additionalHooksadds hooks by name. - The callback is the hook's function argument, and the dependency list follows it:
useImperativeHandle(ref, create, deps)readscreateanddeps. - Missing: a reactive value (prop, state, a value computed during render, whatever
its name's case) the callback reads and the list does not name. Not reactive: a
useState/useReducersetter and auseRefobject (by binding, so also through a renameduseState), module-level values, globals, the callback's own parameters. - Unnecessary: for
useCallback/useMemo, any listed value the callback does not read. For an effect, only a listed value that is not reactive (a module constant): listing a reactive value to re-run the effect when it changes is the documented pattern, so it is never reported. - A dependency argument that is not an array literal cannot be checked and is reported; no dependency argument at all is not judged.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
additionalHooks | string | - | Regex matched against the name of a called custom hook (e.g. useCustomEffect(...)); matching hooks are checked like useEffect |
enableDangerousAutofixThisMayCauseInfiniteLoops | boolean | false | Accepted for compatibility with react-hooks/exhaustive-deps; has no effect (fixes are always offered as suggestions, never applied automatically) |
Configuration Examples
Basic Usage
{
rules: {
'react-features/hooks-exhaustive-deps': 'warn'
}
}With Custom Hooks
{
rules: {
'react-features/hooks-exhaustive-deps': ['warn', {
additionalHooks: '(useMyCustomEffect|useDeepCompareEffect)'
}]
}
}Common Patterns
Object Dependencies
// โ Problem: Object recreated each render
function Component({ filters }) {
useEffect(() => {
search(filters);
}, [filters]); // Runs every render if filters is new object!
}
// โ
Solution: Destructure or memoize
function Component({ filters }) {
const { query, category } = filters;
useEffect(() => {
search({ query, category });
}, [query, category]); // Only primitive values
}Function Dependencies
// โ Problem: Function recreated each render
function Component({ onSuccess }) {
useEffect(() => {
api.subscribe(onSuccess);
return () => api.unsubscribe(onSuccess);
}, [onSuccess]); // May cause re-subscribe every render
}
// โ
Solution: Use ref for stable callbacks
function Component({ onSuccess }) {
const onSuccessRef = useRef(onSuccess);
onSuccessRef.current = onSuccess;
useEffect(() => {
const handler = (...args) => onSuccessRef.current(...args);
api.subscribe(handler);
return () => api.unsubscribe(handler);
}, []); // Stable - no dependencies needed
}Intentionally Omitting Dependencies
// โ
Run effect only once with ESLint directive
function Component({ initialValue }) {
useEffect(() => {
setup(initialValue);
// eslint-disable-next-line react-features/hooks-exhaustive-deps
}, []); // Intentionally run once
}Stale Closure Deep Dive
When Not To Use
| Scenario | Recommendation |
|---|---|
| ๐งช Prototyping | Consider relaxing to reduce noise |
| ๐ Legacy codebase | Enable incrementally |
| ๐ง Complex patterns | Use eslint-disable with comment explaining |
Comparison with Alternatives
| Feature | hooks-exhaustive-deps | react-hooks/exhaustive-deps |
|---|---|---|
| Missing deps | โ Yes | โ Yes |
| Extra deps | โ Yes | โ Yes |
| LLM-Optimized | โ Yes | โ No |
| ESLint MCP | โ Optimized | โ No |
| Suggestions | โ With fix examples | โ ๏ธ Limited |
| Custom hooks | โ Yes | โ Yes |
Related Rules
jsx-key- React key prop validationno-direct-mutation-state- State mutation preventionreact-no-inline-functions- Performance optimization
Further Reading
- React Hooks Dependencies - Official React docs
- A Complete Guide to useEffect - Dan Abramov's deep dive
- Removing Effect Dependencies - When and how to optimize
- ESLint MCP Setup - Enable AI assistant integration
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Dynamic Variable References
Why: Static analysis cannot trace values stored in variables or passed through function parameters.
// โ NOT DETECTED - Prop from variable
const propValue = computedValue;
<Component prop={propValue} /> // Computation not analyzedMitigation: Implement runtime validation and review code manually. Consider using TypeScript branded types for validated inputs.
Imported Values
Why: When values come from imports, the rule cannot analyze their origin or construction.
// โ NOT DETECTED - Value from import
import { getValue } from './helpers';
processValue(getValue()); // Cross-file not trackedMitigation: Ensure imported values follow the same constraints. Use TypeScript for type safety.
Use this rule
npm install -D oxlint-plugin-react-features{ "jsPlugins": ["oxlint-plugin-react-features"], "rules": { "react-features/hooks-exhaustive-deps": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-react-features'], rules: { 'react-features/hooks-exhaustive-deps': 'error', }, },});oxlint-plugin-react-features re-exports eslint-plugin-react-features/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it โ or follow the AI-code-security benchmarks behind these rules.