React Hooks Exhaustive Deps Not Working

React Hooks provide a powerful way to manage side effects in functional components using the useEffect hook. However, many developers encounter issues where the exhaustive deps rule doesn’t seem to work correctly or produces unexpected behavior. This can lead to stale state, unnecessary re-renders, or even infinite loops.

In this guide, we will explore why exhaustive dependencies (deps) might not work, common mistakes, and the best practices to resolve them.

What is useEffect and Exhaustive Dependencies?

The useEffect hook in React allows developers to perform side effects like API calls, subscriptions, or DOM manipulations. It accepts a dependency array that determines when the effect should re-run.

Basic Syntax

useEffect(() => {console.log("Effect runs");});

By default, the effect runs after every render. To control its behavior, we use the dependency array.

Using the Dependency Array

useEffect(() => {console.log("Runs only once");}, []); // Empty dependency array

If we include dependencies, the effect runs whenever those values change:

const [count, setCount] = useState(0);useEffect(() => {console.log(`Effect runs when count changes: ${count}`);}, [count]);

React enforces a rule called exhaustive-deps, which warns developers when they forget dependencies that should be included in the array.

Why is useEffect Exhaustive Deps Not Working?

1. Ignoring ESLint Warnings

One of the main reasons developers face issues with exhaustive deps is ignoring ESLint warnings. React provides linting rules that help identify missing dependencies. If you disable or ignore these warnings, your effect might not behave as expected.

Example of a Common Warning

const [count, setCount] = useState(0);useEffect(() => {console.log(count); // Stale value}, []);

Warning: React Hook useEffect has a missing dependency: ‘count’.

Fix: Include count in Dependencies

useEffect(() => {console.log(count);}, [count]);

2. Mutating Dependencies Inside useEffect

If you modify a dependency inside the effect, React won’t recognize the change properly, leading to unexpected behavior.

Incorrect Example

const [data, setData] = useState([]);useEffect(() => {data.push("new item"); // ❌ Mutating state directly}, [data]);

Since React does shallow comparison of dependencies, it won’t detect changes to objects or arrays if they are mutated directly.

Fix: Use State Updaters

useEffect(() => {setData((prevData) => [...prevData, "new item"]);}, []);

3. Using Functions as Dependencies Without useCallback

If you pass a function as a dependency, React recreates the function on every render, causing unnecessary re-executions of the effect.

Incorrect Example

const fetchData = () => {console.log("Fetching data...");};useEffect(() => {fetchData();}, [fetchData]); // ❌ Function gets re-created

Fix: Wrap Function in useCallback

const fetchData = useCallback(() => {console.log("Fetching data...");}, []);useEffect(() => {fetchData();}, [fetchData]); // ✅ Stable dependency

4. Using Objects or Arrays as Dependencies

React performs shallow comparison on dependency arrays. If an object or array is re-created on every render, the effect will keep re-running.

Incorrect Example

const [count, setCount] = useState(0);useEffect(() => {console.log("Effect runs");}, [{ count }]); // ❌ Always re-runs (new object on each render)

Fix: Use Primitive Values or Memoization

useEffect(() => {console.log("Effect runs");}, [count]); // ✅ Primitive value

Or, if using an object, wrap it in useMemo:

const data = useMemo(() => ({ count }), [count]);useEffect(() => {console.log("Effect runs");}, [data]); // ✅ Memoized object

5. Depending on Props That Change Frequently

If a prop value changes on every render, it will cause the effect to re-run constantly.

Incorrect Example

const MyComponent = ({ items }) => {useEffect(() => {console.log("Effect runs");}, [items]); // ❌ `items` may change frequently};

Fix: Memoize Props Using useMemo or useCallback

const MyComponent = ({ items }) => {const memoizedItems = useMemo(() => items, [items]);useEffect(() => {console.log("Effect runs");}, [memoizedItems]); // ✅ Stable dependency};

6. Using Empty Dependency Array Incorrectly

If an effect relies on state values, but you use an empty dependency array, React will not re-run the effect when those values change.

Incorrect Example

const [count, setCount] = useState(0);useEffect(() => {console.log(count); // ❌ Stale value}, []); // Will only log once

Fix: Add count as a Dependency

useEffect(() => {console.log(count);}, [count]); // ✅ Updates correctly

How to Debug useEffect Exhaustive Deps Issues?

1. Always Check ESLint Warnings

React’s exhaustive-deps rule is there for a reason. Follow its suggestions to avoid common pitfalls.

2. Avoid Directly Mutating Dependencies

Instead of modifying arrays or objects inside useEffect, use state updater functions.

3. Use useCallback and useMemo When Necessary

Memoize functions and objects that are used as dependencies to avoid unnecessary re-renders.

4. Log Dependencies to Debug Issues

If useEffect is not working as expected, log dependencies to check if they are changing.

useEffect(() => {console.log("Effect runs", count);}, [count]);

If your React Hooks exhaustive deps are not working, it is often due to missing dependencies, incorrect dependency tracking, or frequent re-creations of values. To ensure proper functionality:

  • Follow ESLint warnings and include all necessary dependencies.
  • Avoid mutating objects or arrays directly; use useState updates instead.
  • Wrap functions in useCallback and objects in useMemo to stabilize dependencies.
  • Check if props are changing unnecessarily and optimize them with memoization.

By applying these best practices, you can write cleaner, more efficient useEffect hooks and prevent unexpected bugs in your React applications.