How the useEffect Dependency Array Works

The dependency array is the second argument to useEffect. It tells React when to skip re-running the Effect by comparing each value with Object.is.

6 min read

The dependency array is the second argument to useEffect, and it controls when the Effect re-runs. React compares each listed value with its value from the previous render using Object.is. If all of them are the same, React skips the Effect.

Three ways to call useEffect

The same Effect behaves differently depending on the second argument. The table below shows the three options and when each one runs. The choice shapes both correctness and performance, so it is worth getting right.

Second argumentWhen the Effect runs
No arrayAfter every render
Empty arrayOnce after mount, plus one extra cycle in development
List of valuesAfter mount and whenever any listed value changes

Choosing the right form is the difference between a clock that ticks once per second and a connection that reopens on every keystroke. Most Effects should use the third form, listing exactly the props and state they read.

A mount-only Effect

This component listens for window resizes and shows the current width. The listener only needs to be attached once, and it must be removed when the component leaves the screen.

App.jsxApp.jsx
import { useEffect, useState } from "react";
 
function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);
  return <p>The window is {width}px wide</p>;
}

The empty array tells React the Effect reads no reactive values, so it runs only after the first commit. The cleanup removes the listener on unmount, which keeps the page from leaking handlers. Resizing the window updates the number on screen, but it never re-attaches the listener.

Strict Mode still runs this Effect an extra time in development, so the listener is attached, removed, and attached again. The cleanup makes that harmless because only one listener exists at a time.

How dependencies are compared

React checks each dependency with Object.is. Two strings or numbers with the same content are equal, so a string dependency rarely causes an extra run.

Value typeCompared byResult across renders
String or numberContentStays equal when unchanged
Object or functionIdentityNew value every render

Objects and functions are different. A new object created during render is a new value on every render, even when its contents are identical. That is why an object built in the component body makes an Effect re-run on every commit.

Because of this, a primitive is almost always the safest dependency. When an object or function is necessary, define it inside the Effect or move it outside the component. Stale closures in React Hooks explains what happens when a value inside the Effect grows out of date.

A missing dependency hides a bug

If the Effect reads a prop or a state value, that value belongs in the array. Omitting it tells React to keep using a value from an older render.

App.jsxApp.jsx
function Room({ roomId }) {
  useEffect(() => {
    joinRoom(roomId);
    return () => leaveRoom(roomId);
  }, []);
  return <h1>Room {roomId}</h1>;
}

This Effect reads roomId but never lists it, so changing the room leaves the component joined to the old one. The linter flags the missing dependency for exactly this reason.

Never silence the warning to hide the problem. Instead, add the value or move it out of the component so it is no longer reactive. A value declared outside the component cannot change between renders, so it no longer needs to be listed.

When an Effect re-runs forever

An Effect that updates state can start a loop if that state is also a dependency. React runs the Effect, the state update triggers a render, the dependency changes, and the Effect runs again.

The common fix is to pass an updater function instead of reading the state, or to move the calculation into the render body. Infinite loops in useEffect shows how to break that cycle without suppressing the linter.

What to learn next

The dependency array is one half of the Effect story. If an Effect you wrote keeps re-running, the dependency list is the first place to look. For a plain walkthrough of the Hook itself, see React useEffect explained.

Rune AI

Rune AI

Key Insights

  • The dependency array is the second argument to useEffect.
  • No array runs after every render; an empty array runs once on mount.
  • A list runs after mount and when any listed value changes.
  • React compares dependencies with Object.is, so objects and functions differ every render.
RunePowered by Rune AI

Frequently Asked Questions

What does an empty dependency array mean?

It means the Effect runs once after mount, plus one extra setup and cleanup cycle in development under Strict Mode.

How does React compare dependencies?

React compares each dependency with its previous value using Object.is. Primitives compare by value, while objects and functions compare by identity.

Should I suppress the missing dependency lint warning?

No. The warning usually points at a real bug. Change the code so the dependency is either included or no longer reactive.

Conclusion

The dependency array tells React which reactive values an Effect reads. Omit it to run after every render, pass an empty array to run once on mount, or list the values that should trigger a re-sync.