Mastering React useEffect
: A Practical Cheat-Sheet with Real-World Examples & Pitfalls
Introduction
useEffect
is the Swiss-army knife of React hooks—powerful, flexible, and also the easiest to misuse. This cheat-sheet distills years of real-world lessons into one concise reference. Inside, you’ll find:
- A clear mental model of how React schedules effects, cleans them up, and decides when to re-run them.
- Golden rules that prevent stale data, infinite loops, and memory leaks before they happen.
- Copy-paste recipes for the tasks you tackle every day — fetching data, wiring up WebSockets, debouncing searches, synchronising the document title, and more.
- Common pitfalls broken down with side-by-side “wrong vs. right” code so you can spot problems instantly.
- Battle-tested strategies for cancelling or guarding async work, complete with
AbortController
, live flags, sequence numbers, and React Query examples. - Debugging tips to verify effect timing and eliminate unnecessary network calls or duplicate listeners.
Whether you’re polishing a production app, onboarding a teammate, or revisiting React after a break, this guide will help you wield useEffect
with confidence—keeping your components predictable, performant, and easy to maintain.
1 Mental Model — How a Single useEffect
Cycle Works
- Render #N — React paints.
- Effect #N runs (mount or re-run).
- Next render #N + 1 • React calls cleanup #N • Then runs effect #N + 1.
- Unmount — only the last cleanup fires.
Quick reference No dep array ⇒ every render.
[]
⇒ once on mount (cleanup on unmount).[a]
⇒ on mount + whenevera
changes.[a,b,…]
⇒ on mount + when any listed value changes.
2 Golden Rules at a Glance
- List every value you read in the effect inside the dependency array.
- Memoise objects/functions before adding them to deps (
useMemo
,useCallback
). - Return a cleanup for listeners, timers, sockets, observers, etc.
- Split large effects — one clear purpose each.
- Guard async work (abort, live-flag, or sequence).
- Keep ESLint’s
react-hooks/exhaustive-deps
rule enabled.
3 Everyday Patterns — Copy-Paste Blocks
3.1 Fetch when a prop changes
function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const r = await fetch(`/api/users/${userId}`);
setUser(await r.json());
})();
}, [userId]);
return <>{user?.name}</>;
}
3.2 One-off initialisation (e.g. Mapbox map)
useEffect(() => {
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
});
return () => map.remove();
}, []);
3.3 Keep document title in sync with cart size
useEffect(() => {
document.title = `Cart • ${items.length} items`;
}, [items.length]);
3.4 Reconnect WebSocket when trading pair changes
function Ticker({ pair }: { pair: string }) {
const [price, setPrice] = useState<number>();
useEffect(() => {
const ws = new WebSocket(`wss://prices.example/${pair}`);
ws.onmessage = e => setPrice(JSON.parse(e.data).price);
return () => ws.close();
}, [pair]); return <div>{price}</div>;
}
3.5 Debounced live search with cancellation
const [term, setTerm] = useState('');
const debounced = useDebounce(term, 300);
useEffect(() => {
if (!debounced) return;
const ctrl = new AbortController(); fetch(`/api/search?q=${debounced}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setResults)
.catch(e => e.name !== 'AbortError' && console.error(e)); return () => ctrl.abort();
}, [debounced]);
3.6 Track window size once with cleanup
const [size, setSize] = useState({ w: 0, h: 0 });
useEffect(() => {
const handler = () => setSize({ w: innerWidth, h: innerHeight });
addEventListener('resize', handler);
handler(); // initialise
return () => removeEventListener('resize', handler);
}, []);
3.7 Simple animation loop (every render)
const [tick, setTick] = useState(0);
useEffect(() => {
const id = requestAnimationFrame(() => setTick(t => t + 1));
return () => cancelAnimationFrame(id);
}); // ← no deps
4 Pitfalls & Proven Fixes
4.1 Missing Dependencies → Stale Closure
// ❌ WRONG — roomId omitted
useEffect(() => {
fetch(`/api/rooms/${roomId}/messages`).then(/* … */);
}, []);
Fix
useEffect(() => {
fetch(`/api/rooms/${roomId}/messages`).then(/* … */);
}, [roomId]);
4.2 Object / Function Identity Churn
// ❌ Runs every render because {status} is new each time
useEffect(() => {
socket.emit('filter', { status });
}, [{ status }]);
Fix A — create object inside effect:
useEffect(() => {
socket.emit('filter', { status });
}, [status]);
Fix B — memoise object:
const payload = useMemo(() => ({ status }), [status]);
useEffect(() => socket.emit('filter', payload), [payload]);
4.3 Async Race Conditions
Strategy 1 — Live flag
useEffect(() => {
let live = true;
fetch(url)
.then(r => r.json())
.then(d => live && setData(d));
return () => { live = false };
}, [url]);
Strategy 2 — AbortController (Preferred when using native fetch
)
useEffect(() => {
const ctrl = new AbortController();
fetch(url, { signal: ctrl.signal })
.then(r => r.json())
.then(setData)
.catch(e => e.name !== 'AbortError' && console.error(e));
return () => ctrl.abort();
}, [url]);
Strategy 3 — Sequence number
const seq = useRef(0);
useEffect(() => {
const s = ++seq.current;
axios.get(url).then(res => {
if (s === seq.current) setData(res.data);
});
}, [url]);
Strategy 4 — React Query / SWR
const { data } = useQuery(['user', id], () =>
fetch(`/api/users/${id}`).then(r => r.json())
);
4.4 State-Update Loop
// ❌ Infinite loop
useEffect(() => setCount(count + 1), [count]);
Fix
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
4.5 Forgotten Cleanup → Memory Leaks
useEffect(() => {
window.addEventListener('resize', handler);
}, []); // ❌ leaks on unmount
Fix
useEffect(() => {
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
5 Debug Toolkit
- Console logs inside both effect and its cleanup to observe the order.
- React DevTools Profiler — watch effect re-runs.
- why-did-you-render package — highlights useless renders.
- Network tab — duplicate requests reveal race conditions.
6 60-Second Recap
- Describe dependencies → React decides when to rerun.
- Memoise non-primitives and return cleanups.
- Guard async work to avoid stale updates.
Master those and useEffect
stays predictable, efficient, and leak-free.