Skip to main content
interlace
Plugin: react-featuresRules

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

AspectDetails
SeverityWarning (correctness)
Auto-Fix💡 Suggests fixes
CategoryReact
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForAll React projects using hooks

Rule Details

Why This Matters

IssueImpactSolution
🔄 Stale ClosuresOutdated values in callbacksAdd all reactive dependencies
🐛 Missing UpdatesEffect doesn't re-runInclude all used variables
🔁 Infinite LoopsEffect triggers itselfMemoize object/function deps
Performance IssuesUnnecessary effect runsRemove constant dependencies

Hooks Covered

HookDescriptionDependency Array Purpose
useEffectSide effectsWhen to re-run effect
useLayoutEffectSynchronous DOM updatesWhen to re-run effect
useCallbackMemoize functionsWhen to recreate function
useMemoMemoize valuesWhen to recalculate value
useImperativeHandleCustomize ref handleWhen 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

mounted

Configuration

OptionTypeDefaultDescription
additionalHooksstring-Regex for custom hooks to check
enableDangerousAutofixThisMayCauseInfiniteLoopsbooleanfalseAllow 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

ScenarioRecommendation
🧪 PrototypingConsider relaxing to reduce noise
📊 Legacy codebaseEnable incrementally
🔧 Complex patternsUse eslint-disable with comment explaining

Comparison with Alternatives

Featurehooks-exhaustive-depsreact-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

Further Reading

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 analyzed

Mitigation: 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 tracked

Mitigation: Ensure imported values follow the same constraints. Use TypeScript for type safety.

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.