The exhaustive-deps ESLint rule is flagging a dependency mismatch in your React useEffect, useCallback, or useMemo hook, meaning the hook might not re-run when you expect it to, or it might re-run too often, leading to bugs or performance issues.

Here’s a breakdown of the common causes and how to fix them:

1. Missing Dependencies

This is the most frequent culprit. You’re using a variable or function from outside the hook’s scope inside it, but you haven’t declared it as a dependency.

Diagnosis: The ESLint warning will explicitly list the missing dependency. For example: 'myFunction' is missing from useEffect dependencies

Fix: Add the missing dependency to the array.

const myValue = 'some data';

useEffect(() => {
  console.log(myValue);
}, []); // <-- Missing myValue here

Corrected:

const myValue = 'some data';

useEffect(() => {
  console.log(myValue);
}, [myValue]); // Added myValue

Why it works: React’s useEffect (and its cousins) re-runs its callback function when any of the values in its dependency array change. By adding myValue, you tell React to re-run the effect whenever myValue’s content changes.

2. Stale Closure (Function Dependencies)

You’re using a function defined outside the hook, and that function itself relies on variables that change over time. If you don’t include the function as a dependency, the hook will keep using the old version of the function (a "stale closure"), even if the variables it depends on have updated.

Diagnosis: The ESLint warning will point to the function name as a missing dependency. 'handleClick' is missing from useEffect dependencies

Fix: Add the function to the dependency array.

const [count, setCount] = useState(0);

const handleClick = () => {
  console.log('Current count:', count); // This 'count' can become stale
};

useEffect(() => {
  // If 'count' changes, this effect needs to re-run with the updated 'handleClick'
  // But if handleClick isn't a dependency, it might not be.
  const timer = setTimeout(handleClick, 1000);
  return () => clearTimeout(timer);
}, []); // <-- Missing handleClick here

Corrected:

const [count, setCount] = useState(0);

const handleClick = () => {
  console.log('Current count:', count);
};

useEffect(() => {
  const timer = setTimeout(handleClick, 1000);
  return () => clearTimeout(timer);
}, [handleClick]); // Added handleClick

Why it works: When handleClick is added to the dependency array, React will re-create handleClick (and thus the effect) whenever handleClick itself changes. Since handleClick is defined within the component scope and uses count, its definition implicitly changes when count changes, ensuring the effect always uses the latest handleClick.

3. Stale Closure (Object/Array Dependencies)

Similar to functions, if you’re using an object or array defined outside the hook, and its contents change, you need to include it. However, often the object/array itself is re-created on every render (even if its contents are the same), which can cause unnecessary re-runs.

Diagnosis: ESLint flags the object or array. 'options' is missing from useEffect dependencies

Fix (Option A: Add it directly):

const options = { theme: 'dark' };

useEffect(() => {
  console.log('Using options:', options);
}, []); // <-- Missing options

Corrected:

const options = { theme: 'dark' };

useEffect(() => {
  console.log('Using options:', options);
}, [options]); // Added options

Why it works: React will now re-run the effect if the options object reference changes.

Fix (Option B: Memoize the object/array): If the object/array is being re-created unnecessarily, use useMemo or useCallback to stabilize its reference.

const [theme, setTheme] = useState('dark');

// This 'config' object is re-created on every render
const config = {
  theme: theme,
  fontSize: 16,
};

useEffect(() => {
  console.log('Using config:', config);
}, []); // <-- Missing config

Corrected:

const [theme, setTheme] = useState('dark');

const config = useMemo(() => ({
  theme: theme,
  fontSize: 16,
}), [theme]); // Depend on 'theme' so config only re-creates when theme changes

useEffect(() => {
  console.log('Using config:', config);
}, [config]); // Now config is stable when it should be

Why it works: useMemo ensures config is only re-created when theme changes. This stable reference is then correctly added to the useEffect dependency array, preventing unnecessary re-runs.

4. ESLint Rule Configuration Issues

Sometimes, the ESLint configuration itself might be too strict or not configured correctly for your project setup.

Diagnosis: The warning appears inconsistently, or you’ve added dependencies but ESLint still complains. Check your .eslintrc.js (or .json, .yaml) file.

Fix:

  • Disable the rule for a specific line (use with caution):
    useEffect(() => {
      // ...
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);
    
    Why it works: This tells ESLint to ignore the rule for this particular hook. Only do this if you fully understand why the dependency is not needed.
  • Adjust the additionalHooks option: If you have custom hooks that should also be checked, ensure they are listed.
    // .eslintrc.json
    {
      "rules": {
        "react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "(useMyCustomHook|useAnotherOne)" }]
      }
    }
    
    Why it works: This tells ESLint to apply the exhaustive-deps rule to your specified custom hooks in addition to the built-in React ones.
  • Check for conflicting rules: Ensure no other rules are interfering.

5. Dependencies Defined Inside the Hook

You might accidentally define a variable or function inside the hook’s scope that you also intend to be a dependency.

Diagnosis: ESLint will complain about a variable used before it’s declared, or a circular dependency.

Fix: Move the variable/function definition outside the hook, or ensure it’s correctly passed as a dependency.

useEffect(() => {
  const message = 'Hello'; // Defined inside the hook
  console.log(message);
  // If 'message' was meant to be dynamic, it should be a prop or state
  // and thus a dependency.
}, []); // <-- ESLint might warn if 'message' was *intended* to be dynamic

Corrected (if message should be dynamic):

const [message, setMessage] = useState('Hello'); // Defined outside

useEffect(() => {
  console.log(message);
}, [message]); // Now correctly a dependency

Why it works: By defining message as state outside the hook, its changes are tracked by React, and including it in the dependency array ensures the effect re-runs when message is updated.

6. Incorrectly Using useRef for Values That Should Trigger Re-renders

useRef is for mutable values that don’t cause re-renders. If you’re storing a value in useRef that should cause a re-render when it changes, you’re misusing it and the hook won’t see the change.

Diagnosis: Your hook doesn’t re-run when a value stored in a ref changes, but the ref’s .current property is updated.

Fix: Use useState instead of useRef for values that need to trigger component updates.

const timerIdRef = useRef(null);

useEffect(() => {
  timerIdRef.current = setInterval(() => {
    console.log('Tick');
  }, 1000);
  return () => clearInterval(timerIdRef.current);
}, []); // <-- If timerIdRef.current needed to be dynamic, this is wrong

Corrected (if you needed to access the timer ID dynamically later in another effect):

const [timerId, setTimerId] = useState(null);

useEffect(() => {
  const id = setInterval(() => {
    console.log('Tick');
  }, 1000);
  setTimerId(id); // Update state
  return () => clearInterval(timerId); // Use state value for cleanup
}, []); // No dependencies needed here if interval is set once

// If you needed to clear it based on some condition:
useEffect(() => {
  return () => {
    if (timerId) {
      clearInterval(timerId);
    }
  };
}, [timerId]); // Cleanup depends on timerId

Why it works: useState triggers re-renders when its value changes, making the updated value available to subsequent effects or cleanup functions. useRef does not trigger re-renders, so its .current updates are invisible to the React rendering cycle and hook dependency tracking.

After fixing these, the next error you might encounter is a performance warning if you’ve over-corrected and added too many dependencies, causing unnecessary re-renders.

Want structured learning?

Take the full React course →