useEffect is the hook that causes the most bugs, usually because it's reached for when it isn't needed, or wired up without cleanup. Here are the failure modes, in the order they bite.
The infinite loop
An effect that sets state it also depends on re-runs forever. The classic: updating state with no dependency array, or depending on an object/array recreated every render.
// ❌ runs every render → setState → re-render → runs again...
useEffect(() => {
setCount(count + 1);
});
// ✅ run once on mount
useEffect(() => {
setCount((c) => c + 1);
}, []);Missing dependencies
The dependency array must list every reactive value the effect reads. Omit one and the effect uses a stale value; the eslint react-hooks plugin catches these — don't silence it, fix it.
Cleanup: the part people skip
useEffect(() => {
const id = setInterval(tick, 1000);
const onResize = () => measure();
window.addEventListener("resize", onResize);
// cleanup runs on unmount AND before the next effect run
return () => {
clearInterval(id);
window.removeEventListener("resize", onResize);
};
}, []);Race conditions in data fetching
Fetch in an effect and the user navigates fast? An older request can resolve AFTER a newer one and overwrite fresh data with stale. Guard with an ignore flag (or AbortController).
useEffect(() => {
let ignore = false;
fetchUser(id).then((u) => {
if (!ignore) setUser(u); // drop the result if we've moved on
});
return () => { ignore = true; };
}, [id]);Often you don't need an effect at all
Transforming props/state for render? Do it during render, not in an effect. Responding to a user event? Do it in the event handler. Effects are for synchronising with something OUTSIDE React — the DOM, a subscription, a timer, the network — not for reacting to your own state.
Before writing an effect, ask: am I syncing with an external system? If not, there's usually a simpler place for the logic. When you do need one, list all deps and return a cleanup. That alone removes most useEffect bugs.