B9 · Part 1 · Beginner
useEffect basics
For synchronising with something outside React — and most of the time you do not need it.
09On this page
What an effect is for
Rendering is pure: it computes JSX and touches nothing else. Effects are the escape hatch for the things rendering must not do — talking to a subscription, a timer, the document title, a browser API. React runs them after the DOM has been updated.
useEffect(() => {
document.title = title;
}, [title]);
The dependency array
| Written as | Runs |
|---|---|
useEffect(fn) | After every render |
useEffect(fn, []) | Once, after the first render |
useEffect(fn, [a, b]) | After the first render, then whenever a or b changes |
React compares each dependency with Object.is against the
previous render's value. That is a reference comparison, which is where most
of the surprises come from.
Cleanup
Return a function and React calls it before the next run of the effect, and once more when the component unmounts. Anything you start, you clean up.
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
Without the cleanup, every remount adds another interval or another listener. They accumulate, they all keep firing, and they hold whatever they closed over in memory.
Why your effect runs twice in development
In development, Strict Mode mounts, unmounts and remounts every component once on purpose. It is not a bug and it does not happen in production — it exists to reveal effects with missing cleanup. If running twice breaks something, that thing would also break when a user navigated away and back.
Fetching, done properly
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch("/api/users/" + userId)
.then((r) => r.json())
.then((data) => {
if (!cancelled) setUser(data); // ignore a stale response
})
.catch((err) => {
if (!cancelled) setError(err);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true; // this is the race fix
};
}, [userId]);
The cancelled flag is the whole point. Change
userId from 1 to 2 quickly and both requests are in flight; if
request 1 is slower, it resolves last and overwrites user 2 with user
1. The screen shows the wrong person and nothing looks broken. Every fetching
effect without that flag has this bug, and it only shows up on slow
connections.
AbortController does the same job and also cancels the request
itself — worth using once you are comfortable. In real projects, a data
library handles all of this;
data fetching patterns covers when to
stop hand-rolling it.
When exactly it runs
- You set state, React re-renders the component.
- React applies the changes to the DOM.
- The browser paints.
- Then effects run — children before parents.
Because it runs after paint, an effect that measures the DOM and then changes
layout produces a visible flicker: the user sees the first version. That is
what useLayoutEffect is for — identical API, but it runs
before paint, so the browser never shows the intermediate state. It
blocks painting, so use it only for measuring and adjusting, never for
fetching.
How dependencies are compared
useEffect(fn, [user.id]); // a string — compares by value ✓
useEffect(fn, [user]); // an object — compares by reference
useEffect(fn, [{ id }]); // ✗ new object every render → runs every render
useEffect(fn, [items.length]); // ✓ a number
React uses Object.is on each entry. Primitives compare by value,
everything else by identity. If a dependency is an object or a function
created during render, it is new every time — which is the single most
common cause of an effect that will not stop running.
The array length must also be constant between renders. React compares position by position, so a conditionally built dependency array is the same class of bug as a conditional hook.
Most effects should not exist
This is the part that separates people who use the hook from people who understand it. Before writing one, check whether it belongs to one of these categories — none of which needs an effect.
Deriving a value
// ✗ an extra render, and it can go stale
const [full, setFull] = useState("");
useEffect(() => { setFull(first + " " + last); }, [first, last]);
// ✓ just compute it
const full = first + " " + last;
Responding to a user action
// ✗ now every path that changes items sends analytics
useEffect(() => { if (items.length) track("added"); }, [items]);
// ✓ it happened because of a click, so put it in the click
function handleAdd(item) {
setItems([...items, item]);
track("added");
}
The test is simple: did this happen because the user did something, or because the component appeared on screen? The first belongs in a handler. Only the second is an effect.
Resetting state when a prop changes
// ✗ renders once with the old value first
useEffect(() => { setDraft(""); }, [userId]);
// ✓ let React throw the component away
<Editor key={userId} userId={userId} />
The two loops
useEffect(() => { setCount(count + 1); }); // ✗ no array — infinite
useEffect(() => {
setUser({ ...user, seen: true }); // ✗ new object each time
}, [user]);
The second is the one that catches people out. Objects, arrays and functions
created during render are new references every render, so an effect that
depends on one runs every render — and if it sets state, forever. Depend
on a primitive instead ([user.id]), or create the value outside
the component.
Do not lie to the linter
The exhaustive-deps rule tells you which values your effect reads. Removing one to stop the effect re-running does not fix the loop; it freezes the effect on an old value and turns an obvious bug into a subtle one. If satisfying the linter breaks the effect, the effect is modelling the wrong thing.
Interview answer, one sentence
"An effect synchronises the component with something outside React, and it
runs after paint with a cleanup that runs before the next one. The
interesting part is what is not an effect — derived values,
anything caused by a user action, and resetting state on a prop change,
which is a key."
Opens in the editor: write it, run it, and check it against real tests.
All 46 for this level →Finished reading?
0 of 57 chapters read in this series