Quiz

What does the dependency array of `useEffect` affect?

Topics
React

TL;DR

The dependency array of useEffect determines when the effect should re-run. If the array is empty, the effect runs only once after the initial render. If it contains variables, the effect runs whenever any of those variables change. If omitted, the effect runs after every render.


What does the dependency array of useEffect affect?

Introduction to useEffect

The useEffect hook in React is used to perform side effects in functional components. These side effects can include data fetching, subscriptions, or manually changing the DOM. The useEffect hook takes two arguments: a function that contains the side effect logic and an optional dependency array.

Dependency array

The dependency array is the second argument to the useEffect hook. It is an array of values that the effect depends on. React uses this array to determine when to re-run the effect.

useEffect(() => {
// Side effect logic here
}, [dependency1, dependency2]);

How the dependency array affects useEffect

  1. Empty dependency array ([]):

    • The effect runs once after the initial render and its cleanup runs once on unmount.
    • This is roughly analogous to componentDidMount plus componentWillUnmount, but with an important caveat: in development with Strict Mode, React intentionally mounts, unmounts, and remounts each component to surface bugs in effect cleanup. Your effect (and its cleanup) will run twice on the initial mount in development. Production runs the effect once.
    useEffect(() => {
    // This code runs after the initial render
    return () => {
    // Cleanup runs on unmount
    };
    }, []);
  2. Dependency array with variables:

    • The effect runs after the initial render and whenever any of the specified dependencies change.
    • React compares each dependency with its previous value using Object.is (reference equality, not a deep or shallow object comparison). Inline objects, arrays, or functions therefore change identity on every render unless memoized.
    useEffect(() => {
    // This code runs after the initial render and whenever dependency1 or dependency2 changes
    }, [dependency1, dependency2]);
  3. No dependency array:

    • The effect runs after every render.
    • This can lead to performance issues if the effect is expensive.
    useEffect(() => {
    // This code runs after every render
    });

Cleanup behavior on dependency change

When dependencies change, React first runs the previous effect's cleanup function (if any) before running the effect again with the new values. The same is true on unmount. This means dependency changes effectively perform a tear-down/set-up cycle, which matters for subscriptions, intervals, and event listeners.

useEffect(() => {
const subscription = source.subscribe(id);
return () => subscription.unsubscribe(); // runs before next effect or on unmount
}, [id]);

The react-hooks/exhaustive-deps lint rule

The official eslint-plugin-react-hooks ships an exhaustive-deps rule that warns when reactive values used inside an effect are missing from the dependency array. Treat its warnings as bugs — silencing the rule is almost always the wrong fix and a common source of stale closures.

Common pitfalls

  1. Stale closures:

    • If you use state or props inside the effect without including them in the dependency array, you might end up with stale values.
    • Always include all state and props that the effect depends on in the dependency array.
    const [count, setCount] = useState(0);
    useEffect(() => {
    const handle = setInterval(() => {
    console.log(count); // This might log stale values if `count` is not in the dependency array
    }, 1000);
    return () => clearInterval(handle);
    }, [count]); // Ensure `count` is included in the dependency array

    In React 19, useEffectEvent (now stable) provides a way to read the latest value of a prop or state from inside an effect without making the effect re-subscribe on every change. It is the recommended escape hatch for the "I want the latest value, but I do not want this effect to re-run" pattern:

    const onTick = useEffectEvent(() => {
    console.log(count); // always reads the latest count
    });
    useEffect(() => {
    const handle = setInterval(onTick, 1000);
    return () => clearInterval(handle);
    }, []); // effect does not need `count` in its deps
  2. Functions as dependencies:

    • Functions are recreated on every render, so including them in the dependency array can cause the effect to run more often than necessary.
    • Use useCallback to memoize functions if they need to be included in the dependency array.
    const handleClick = useCallback(() => {
    console.log('clicked');
    }, []);
    useEffect(() => {
    window.addEventListener('click', handleClick);
    return () => window.removeEventListener('click', handleClick);
    }, [handleClick]); // stable identity, so this effect does not re-subscribe each render

Further reading

Edit on GitHub