Why do we need the dependency array in useEffect
The dependency array tells React WHEN to re-run the effect — it compares deps between renders and re-runs only if one changed. No array = every render; []= once on mount; [a,b] = when a or b changes. It also defines which values the effect 'sees'; omitting deps causes stale closures or infinite loops.
The dependency array answers one question: "when should this effect run again?"
What it controls
A useEffect callback runs after render. The dependency array tells React how to decide whether to re-run it on subsequent renders — React compares each dependency to its value from the previous render (with Object.is) and re-runs the effect only if at least one changed.
useEffect(fn); // NO array → runs after EVERY render
useEffect(fn, []); // empty → runs ONCE (after mount), cleanup on unmount
useEffect(fn, [a, b]); // with deps → runs on mount + whenever a or b changesWhy it's necessary — the two failure modes without it
1. No array → infinite loops & wasted work. If the effect sets state, and it runs after every render, then: effect → setState → re-render → effect → setState → … infinite loop. Even without setState, re-subscribing/re-fetching every render is wasteful.
2. Wrong/empty array → stale closures. The effect's callback is a closure — it captures the values from the render it was created in. If you use count inside the effect but leave it out of deps, the effect keeps running with the stale count from the first render. The deps array isn't just "when to run" — it's "which values is this effect allowed to see." Every reactive value the effect uses must be listed, or the effect goes stale.
So the deps array does two jobs
- Performance / correctness of timing — don't re-run when nothing relevant changed.
- Synchronization correctness — the effect stays in sync with the current values it depends on. React's mental model: an effect synchronizes your component with something external, and the deps are the inputs to that synchronization.
The rules
- List every reactive value the effect uses (props, state, derived values, functions). The
exhaustive-depsESLint rule enforces this — trust it. - If a dependency changes too often (a function/object recreated each render), don't lie by removing it — fix the dependency:
useCallback/useMemoit, move it inside the effect, or use a functional state updater / ref.
The framing
"It tells React when to re-run the effect — it diffs the deps against the previous render and re-runs only on a change. No array runs every render, which causes infinite loops if the effect sets state; [] runs once. But it's not only about timing — the deps array also defines which values the effect's closure sees. Omit a value the effect uses and you get a stale closure: it keeps using the old value. So the rule is list every reactive value the effect uses, and if a dep churns too often, fix the dep — don't remove it to silence the linter."
Follow-up questions
- •What's the difference between no array, [], and [deps]?
- •What is a stale closure and how does the deps array cause it?
- •What do you do if a dependency changes on every render?
- •Why shouldn't you just disable the exhaustive-deps lint rule?
Common mistakes
- •Omitting the array and causing an infinite render loop.
- •Leaving out a value the effect uses — stale closure bugs.
- •Disabling exhaustive-deps instead of fixing the real dependency.
- •Putting an unstable object/function in deps so the effect runs every render.
Performance considerations
- •A correct deps array prevents redundant effect runs (re-fetches, re-subscriptions) and infinite loops. An over-broad array causes unnecessary effect work; a too-narrow one causes stale-data bugs.
Edge cases
- •A function or object dependency recreated every render.
- •An effect that should run once but uses values that change.
- •Effects depending on each other's state updates.
- •Object/array deps compared by reference, not value.
Real-world examples
- •A fetch effect keyed on [userId] so it refetches when the id changes.
- •A subscription effect with [] that subscribes on mount and cleans up on unmount.