Skip to main content
ESLint 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.