Effects
where React breaks beginners most often
What it is
useEffect is a way to do something that isn't drawing: request data,
subscribe to an event, set a timer. It runs after the component has been
drawn.
It is the hardest place in React and the most frequent place for AI mistakes. One forgotten line turns an application into an endless loop of requests.
What it looks like in code
useEffect(() => {
// what to do
return () => { /* how to clean up after yourself */ };
}, [dependencies]);Three parts, and each one matters:
The body — what to run after the render.
The returned function — the cleanup: unsubscribe, cancel the timer, abort the request. It runs before the effect's next run and when the component is removed.
The dependency array — when to repeat. And that is the source of all the trouble.
The dependency array decides everything
| Written as | When it runs |
|---|---|
| no array | after every render |
[] | once, when the component appears |
[userId] | on appearance and every time userId changes |
The difference between the first two rows, measured:
useEffect(() => { // no array
setData([...data, "x"]);
});useEffect(() => { // empty array
setData(["x"]);
}, []);no dependency array: effect ran 49 times
with empty array [] : effect ran 1 timesForty-nine times — and only because I put a limiter in. Without one the loop would be endless: the effect changes the state → the component redraws → the effect runs again.
Cleanup
useEffect(() => {
const controller = new AbortController();
fetch(`/api/subs/${userId}`, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== "AbortError") console.error(e); });
return () => controller.abort();
}, [userId]);Why the returned function is needed: the user switched city quickly — the first request is still in flight and a second one goes out. Without cancellation the responses can arrive out of order, and the previous city's data ends up on the screen.
That is a race condition, and an AI forgets about it almost every time: the
generated code has a fetch but neither an AbortController nor a returned
cleanup function.
When an effect isn't needed
The main misconception is that everything belongs inside a useEffect. In
fact it is only needed for reaching outside — the network, timers,
subscriptions, direct work with the DOM.
Not needed:
// bad: extra state and an extra redraw
const [total, setTotal] = useState(0);
useEffect(() => { setTotal(items.length); }, [items]);
// good: just compute it
const total = items.length;If a value can be computed from what you already have, compute it right during the render. State and an effect here only add a step and a redraw.
What to tell the AI
This useEffect has no dependency array and changes state
inside. Fix it and explain what was wrong.Add request cancellation with AbortController and a returned
cleanup function. Don't show an AbortError to the user — that
isn't an error, it's a cancellation.Are useState and useEffect needed here? Could this value just
be computed during the render?That last question is worth asking every time you see a useEffect that
only sets state from other data.
What to look for
No dependency array. Checked instantly, breaks everything.
An array that is there but incomplete. The effect uses a variable that
isn't in the dependencies — it will work with a stale value. A linter with
the react-hooks/exhaustive-deps rule catches this automatically.
No cleanup on subscriptions and timers. A setInterval without
clearInterval keeps running after the component is removed.
fetch without cancellation. A race condition on fast switching.
An effect instead of a computation. Extra state, an extra redraw.
A request in an effect with no error handling. The server answered 500
— on screen, an endless loading state.
Cheat sheet
useEffectis for reaching outside: the network, timers, subscriptions.Dependency array: none — every time,
[]— once,[x]— whenxchanges.No array plus a state change = an endless loop.
Return a cleanup function for subscriptions, timers and requests.
fetchwithout anAbortControllergives a race condition.If the value can be computed from data you have, no effect is needed.