hooks-exhaustive-deps
hooks-exhaustive-deps rule
Keywords: React, hooks, useEffect, useCallback, useMemo, dependencies, stale closure, ESLint rule, performance, LLM-optimized
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
mountedConfiguration
| Option | Type | Default | Description |
|---|---|---|---|
additionalHooks | string | - | Regex for custom hooks to check |
enableDangerousAutofixThisMayCauseInfiniteLoops | boolean | false | Allow autofix (may cause issues) |
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.