- Home
- /
- Tutorials
- /
- React Tutorial
- /
- Effect Dependencies
Effects and Refs
Effect Dependencies
The dependency array is not a schedule. It is a list of values the effect reads, and React re-runs the effect whenever one of them differs from last time.
The three forms
- No array - runs after every render.
- Empty array - runs once, after the first render.
- Array with values - runs again whenever one of them changes.
All three, counted
jsx
function Deps() {
const [a, setA] = React.useState(0)
const [b, setB] = React.useState(0)
const counts = React.useRef({ every: 0, once: 0, onA: 0 })
React.useEffect(() => { counts.current.every++ })
React.useEffect(() => { counts.current.once++ }, [])
React.useEffect(() => { counts.current.onA++ }, [a])
return (
<div>
<p>a = {a}, b = {b}</p>
<button onClick={() => setA(a + 1)}>Change a</button>{" "}
<button onClick={() => setB(b + 1)}>Change b</button>
<pre style={{ background: "#f1f5f9", padding: 8 }}>
{`no array : ${counts.current.every}
empty [] : ${counts.current.once}
[a] : ${counts.current.onA}`}
</pre>
<p style={{ color: "#64748b" }}>Press "Change b" a few times and watch which counters move.</p>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Deps />)Changing b moves the first counter but not the third. The effect with [a] does not care about b, and React can tell because a is unchanged.
The infinite loop
An effect that sets state it also depends on will run forever: set state, re-render, dependency changed, run again. React eventually errors with Maximum update depth exceeded.
Never do this
jsx
useEffect(() => {
setCount(count + 1) // changes count...
}, [count]) // ...which re-triggers this effectThe same happens more subtly with objects and arrays. {} is a new object every render, so [options] is different every time even when the contents are identical.
Objects break dependency checks
jsx
function Runs() {
const [tick, setTick] = React.useState(0)
const runs = React.useRef({ object: 0, primitive: 0 })
// A new object each render — never equal to the last one.
const options = { pageSize: 10 }
React.useEffect(() => { runs.current.object++ }, [options])
// A primitive compares by value, so this is stable.
React.useEffect(() => { runs.current.primitive++ }, [options.pageSize])
return (
<div>
<button onClick={() => setTick(tick + 1)}>Re-render ({tick})</button>
<pre style={{ background: "#f1f5f9", padding: 8 }}>
{`[options] ran ${runs.current.object} times
[options.pageSize] ran ${runs.current.primitive} times`}
</pre>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Runs />)Do not lie to the array
Removing a dependency to stop an effect re-running does not fix anything - it just makes the effect read a stale value from an old render. The lint rule that complains about this is right; the fix is to change what the effect does, not what you declare.
How React compares them
React compares each dependency with the previous render's value using Object.is - the same check === makes, give or take NaN. There is no deep comparison, and there is no way to ask for one.
That single fact explains every surprising re-run: two objects with identical contents are different values, so an effect depending on one runs every render. The fixes are to depend on a primitive inside it, build the object outside the component, or memoise it - covered in the performance section.
useLayoutEffect, briefly
useLayoutEffect has the identical signature and dependency rules, but runs before the browser paints rather than after. That makes it slower - the browser waits for it - and it is the right choice in exactly one situation: when you measure the DOM and immediately change it based on the measurement.
Doing that in a normal effect means the user sees one frame of the unadjusted layout before it jumps. Positioning a tooltip against its trigger is the standard example. For everything else, useEffect is correct and cheaper.
- List every value from the component that the effect reads.
- Prefer primitives; objects and arrays compare by identity.
- Setters from
useStateare stable and never need listing. - Comparison is
Object.is- never deep. - Use
useLayoutEffectonly to avoid a visible layout jump. - If the list feels wrong, the effect is probably doing too much.
