Skip to content

React Tutorial React Hooks

useEffect Dependencies Explained

Lesson 61 of 156 3 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

The dependency array is where most useEffect bugs come from. The rule is simple, even if it feels strict at first.

The rule

> Every reactive value that the effect reads must be listed in the dependency array.

A reactive value is a prop, a piece of state, or anything you calculate from them inside the component. The array tells React: “run this effect again whenever one of these changes.”

You do not choose the dependencies. The code of the effect decides them. If the effect reads userId, then userId is a dependency.

App.jsx
JSX
function App() {
  const [userId, setUserId] = React.useState(1);
  const [status, setStatus] = React.useState("");

  React.useEffect(() => {
    setStatus("Loaded user " + userId);        // reads userId...
  }, [userId]);                                 // ...so userId is a dependency

  return (
    <div>
      <p>User {userId}</p>
      <p>{status}</p>
      <button onClick={() => setUserId(userId + 1)}>Next user</button>
    </div>
  );
}

What happens if you leave one out?

The effect keeps using the value from an old snapshot. This is called a stale closure:

App.jsx
JSX
function App() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // always sees count = 0, so it sets 1 every time
    }, 1000);
    return () => clearInterval(id);
  }, []); // BUG: count is missing

  return <p>Count: {count}</p>;
}

The timer ticks every second, but the count stops at 1. The effect captured count = 0 on the first render and never saw a newer value. There are two correct fixes:

Fix 1: add the dependency. The timer is recreated whenever count changes:

App.jsx
JSX
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, [count]);

Fix 2 (usually better): use an updater function, so the effect no longer reads count:

App.jsx
JSX
function App() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    const id = setInterval(() => setCount((c) => c + 1), 1000);
    return () => clearInterval(id);
  }, []); // nothing from the component is read, so no dependencies

  return <p>Count: {count}</p>;
}

setCount itself is guaranteed stable, so it never needs to be listed.

Do not lie to React

It is tempting to silence the lint warning by deleting dependencies, or by adding // eslint-disable-next-line. That hides the bug rather than fixing it. If you do not want the effect to re-run when something changes, change the code, not the array:

  • Move the value inside the effect if it is only used there.
  • Use an updater function for state updates.
  • Move a function that does not use component values outside the component.
  • For the rare “read the latest value without re-running” case, React 19.2 added useEffectEvent; see Other Built-in Hooks.

Objects, arrays and functions

Dependencies are compared with Object.is. An object or function created during render is a new one every render, so an effect that depends on it re-runs every time:

App.jsx
JSX
function Chat({ roomId }) {
  const options = { room: roomId };            // new object each render!
  useEffect(() => { connect(options) }, [options]);  // runs every render
}

Create the object inside the effect instead, and depend on the primitive value:

App.jsx
JSX
useEffect(() => {
  const options = { room: roomId };
  connect(options);
}, [roomId]);   // a string: compared by value

Infinite loops

If an effect sets state that is in its own dependency list, and it sets a new value each time, you get a loop:

App.jsx
JSX
useEffect(() => {
  setItems([...items, "x"]);   // changes items...
}, [items]);                   // ...which re-runs the effect: loop

React stops this after a while with an error (“Maximum update depth exceeded”). Often it is a sign that you should not use an effect at all, because the value can be calculated while rendering.

The linter does the work

The Vite template ships with Oxlint, and its react plugin can check hook dependencies, as eslint-plugin-react-hooks does in ESLint (react-hooks/exhaustive-deps). Treat its suggestions as the truth about what your effect reads.

Key points

  • List every prop, state value and derived value that the effect reads.
  • Fix stale values by changing the code (updater functions, moving things inside), not by deleting dependencies.
  • Objects and functions created during render change every time; use primitives or create them inside the effect.