useEffect (and when NOT to)
Sync with external systems. Cleanup. Race conditions.
useEffect is the most misunderstood React hook. The official docs literally have a page called You Might Not Need an Effect, and most useEffect code in real codebases is wrong. Let's set the record straight.
What it's actually for
Use useEffect to synchronize your component with something outside React. Browser APIs, third-party libraries, network requests, subscriptions, timers. If you're not touching the world outside React, you almost certainly don't need an effect.
useEffect(() => {
// 1. Setup: runs after the DOM commits
document.title = `(${count}) Notifications`;
// 2. Cleanup (optional): runs before the next effect or on unmount
return () => {
document.title = "App";
};
}, [count]); // 3. Dependency array: re-run when these changeuseEffect(() => setFoo(bar.length), [bar]), delete it. Just compute const foo = bar.length during render. Effects for derived values are a bug: an extra render, stale values mid-render, and harder reasoning.The infinite loop trap
Every useEffect beginner writes this at least once:
// 🔥 INFINITE LOOP 🔥
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/data").then((r) => r.json()).then(setData);
}); // <- no dependency array! Runs after EVERY render.
// setData renders → effect runs → fetches → setData → renders → ...Three forms of the dependency array, three meanings:
- No array: run after every render. Usually a bug.
[]: run once after mount, cleanup on unmount.[a, b]: run whenaorbchanges (compared withObject.is).
Cleanup matters
Effects often start something that needs stopping. Subscriptions, intervals, listeners, animations. Return a function from your effect and React calls it before the next effect or when the component unmounts.
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id);
}, []);Forgetting cleanup is the most common source of memory leaks and zombie subscriptions in React apps.
Fetching: use AbortController
Network requests started in an effect outlive the component. If the user clicks away before the response, you'll setData on an unmounted component (a warning) or overwrite newer data with older data (a real bug). Abort.
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then((r) => r.json())
.then(setUser)
.catch((e) => {
if (e.name === "AbortError") return; // expected
console.error(e);
});
return () => ctrl.abort();
}, [id]);StrictMode runs your effect twice
In development, React's <StrictMode> intentionally mounts, unmounts, and remounts every component to surface bugs in your effects. You'll see your fetch fire twice, your interval start twice, your console.log appear twice. This is nota bug. It's React shaking your code to make sure your cleanup works.
If StrictMode breaks your effect, your effect was already broken. Either it's missing a cleanup, or it has a side effect that shouldn't be in an effect at all. Production runs them once.
The "you might not need an effect" checklist
Before you write an effect, ask:
- Computing from props/state? Just compute during render.
- Resetting state when a prop changes? Use the
keyprop on the component instead. - Sharing logic between events? Extract a function and call it from each handler.
- Notifying a parent of a change?Lift the state up. Call the parent's callback from the same handler that changes state.
Try it: a real fetch effect with cleanup
Click between users. The effect fetches whenever id changes, aborts the previous request, and cleans up. Open the console to see how StrictMode runs everything twice in dev.
Quiz
When should you reach for useEffect?
Recap
- Effects sync your component with external systems. They are not for derived values.
- Three dep array meanings: omitted = every render,
[]= once,[deps]= on change. - Return a cleanup function. Subscriptions, intervals, fetches all need one.
- Cancel fetches with
AbortControllerin cleanup to avoid race conditions. - StrictMode double-invokes effects in dev. If that breaks you, your cleanup is missing.
- Read "You Might Not Need an Effect" on react.dev. Most uses have a better solution.