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):
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.useEffect(() => { // ... // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - Adjust the
additionalHooksoption: If you have custom hooks that should also be checked, ensure they are listed.
Why it works: This tells ESLint to apply the// .eslintrc.json { "rules": { "react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "(useMyCustomHook|useAnotherOne)" }] } }exhaustive-depsrule 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.