Skip to main content
interlace
Plugin: react-featuresRules

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

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

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 called useEffect is not checked. additionalHooks adds hooks by name.
  • The callback is the hook's function argument, and the dependency list follows it: useImperativeHandle(ref, create, deps) reads create and deps.
  • 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 / useReducer setter and a useRef object (by binding, so also through a renamed useState), 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

OptionTypeDefaultDescription
additionalHooksstring-Regex matched against the name of a called custom hook (e.g. useCustomEffect(...)); matching hooks are checked like useEffect
enableDangerousAutofixThisMayCauseInfiniteLoopsbooleanfalseAccepted 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

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.

Use this rule

npm install -D oxlint-plugin-react-features
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-react-features"],  "rules": {    "react-features/hooks-exhaustive-deps": "error"  }}
vite.config.ts
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.