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 argument | When the Effect runs |
|---|---|
| No array | After every render |
| Empty array | Once after mount, plus one extra cycle in development |
| List of values | After 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.
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 type | Compared by | Result across renders |
|---|---|---|
| String or number | Content | Stays equal when unchanged |
| Object or function | Identity | New 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.
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
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.
Frequently Asked Questions
What does an empty dependency array mean?
How does React compare dependencies?
Should I suppress the missing dependency lint warning?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.