useEffect, what runs when, and the infinite-loop bug everyone hits
useEffect is React's "do something after rendering" escape hatch. Talk to the network, set a timer, sync with a non-React library. Anything that touches the world outside your component.
It is also where 80% of React bugs live. Let us defuse it.
When effects run
The signature is useEffect(fn, deps). React calls fn after the component renders to the DOM, based on deps.
- No deps array:
useEffect(fn). Runs after every render. Almost always wrong. - Empty deps array:
useEffect(fn, []). Runs once after the first render. Good for one-time setup. - Filled deps array:
useEffect(fn, [userId]). Runs after the first render, and again wheneveruserIdchanges.
useEffect(() => {
document.title = `Cart (${count})`;
}, [count]);Cleanup, the part everyone forgets
Return a function from your effect. React calls it before the next effect run, and when the component unmounts.
useEffect(() => {
const id = setInterval(() => tick(), 1000);
return () => clearInterval(id);
}, []);Timers, subscriptions, event listeners, websockets; if you set it up, tear it down. The first Swiggy-style live-order page I built leaked three intervals per second. The browser tab ate 1.2 GB before I figured it out.
The infinite-loop classic
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/restaurants')
.then(r => r.json())
.then(setData);
}); // no deps, runs every render, setData re-renders, runs againAdd [] and it runs once. Add [userId] and it refetches when the user changes. The dep array is not optional decoration; it is the contract.
If your linter (
react-hooks/exhaustive-deps) yells about a missing dependency, listen. It is almost never wrong.
When NOT to use useEffect
This is the lesson nobody tells you in 2026.
- Derivable from props or state? Just calculate it inline. No effect needed.
- Reacting to a user event? Put the logic in the event handler, not in an effect.
- Transforming data for render? Do it during render.
// Wrong
useEffect(() => {
setFullName(first + ' ' + last);
}, [first, last]);
// Right
const fullName = first + ' ' + last;Modern React thinking: useEffect is for syncing with external systems. Network, browser APIs, third-party widgets. Not for "running code when something changes". The new docs literally have a page titled "You Might Not Need an Effect", and it is correct.
Quick checklist
- One concern per effect. If you are subscribing AND fetching AND setting a title, that is three effects.
- Always return cleanup for anything subscription-like.
- In dev, effects run twice on mount (Strict Mode). That is on purpose, to surface missing cleanup. Do not "fix" it by removing Strict Mode.
Next lesson: lists, keys, and forms; the three things React makes weirdly fiddly.
Free tools you can use while you learn
Common questions
Q.When does useEffect run?›
Q.How do I avoid an infinite loop in useEffect?›
Watching quietly. Tap me if you want a tip.
Try this (0 of 1 done)
- 1
Add a useEffect with empty deps that logs "mounted" once.
show answer
function App() { React.useEffect(() => { console.log('mounted'); }, []); return <p>see console</p>; }