Sitemap

Mastering React useEffect: A Practical Cheat-Sheet with Real-World Examples & Pitfalls

4 min readJun 5, 2025

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

  1. Render #N — React paints.
  2. Effect #N runs (mount or re-run).
  3. Next render #N + 1 • React calls cleanup #N • Then runs effect #N + 1.
  4. Unmount — only the last cleanup fires.

Quick reference No dep array ⇒ every render. [] ⇒ once on mount (cleanup on unmount). [a] ⇒ on mount + whenever a 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

  1. Describe dependencies → React decides when to rerun.
  2. Memoise non-primitives and return cleanups.
  3. Guard async work to avoid stale updates.

Master those and useEffect stays predictable, efficient, and leak-free.

--

--

Ly Channa
Ly Channa

Written by Ly Channa

Highly skilled: REST API, OAuth2, OpenIDConnect, SSO, TDD, RubyOnRails, CI/CD, Infrastruct as Code, AWS.

No responses yet