React Cheatsheet
useEffect
Use this React reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Signature
import { useEffect } from 'react'; useEffect( () => { // setup function // side effect here return () => { // optional cleanup function // runs before next effect and on unmount }; }, [dep1, dep2] // dependency array );
Dependency Array Variants
// 1. No dependency array — runs after EVERY render useEffect(() => { console.log('rendered'); }); // 2. Empty array — runs once after initial mount useEffect(() => { console.log('mounted'); return () => console.log('unmounted'); }, []); // 3. With dependencies — runs after mount AND whenever deps change useEffect(() => { console.log('userId changed:', userId); }, [userId]); // 4. Multiple dependencies useEffect(() => { fetchData(userId, filter); }, [userId, filter]);
ESLint rule
react-hooks/exhaustive-depswarns if you forget a dependency. Every reactive value used inside the effect (props, state, functions defined in the component) must be in the array — or you'll get stale closures.
Common Use Cases
Data Fetching
function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; // prevent setting state after unmount setLoading(true); setError(null); fetch(`/api/users/${userId}`) .then(r => r.json()) .then(data => { if (!cancelled) setUser(data); }) .catch(err => { if (!cancelled) setError(err.message); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [userId]); if (loading) return <p>Loading…</p>; if (error) return <p>Error: {error}</p>; return <div>{user?.name}</div>; }
AbortController (preferred over flag)
useEffect(() => { const controller = new AbortController(); fetch(`/api/data/${id}`, { signal: controller.signal }) .then(r => r.json()) .then(setData) .catch(err => { if (err.name !== 'AbortError') setError(err.message); }); return () => controller.abort(); }, [id]);
Subscriptions and Event Listeners
// Window event listener useEffect(() => { const handler = (e) => setSize({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', handler); handler(); // call immediately to set initial value return () => window.removeEventListener('resize', handler); }, []); // Custom event bus / observable useEffect(() => { const sub = store.subscribe(setState); return () => sub.unsubscribe(); }, []);
Timers
// Interval useEffect(() => { const id = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(id); }, []); // Timeout useEffect(() => { const id = setTimeout(() => setVisible(false), 3000); return () => clearTimeout(id); }, []);
DOM Manipulation and Third-party Libraries
function MapWidget({ center }) { const containerRef = useRef(null); const mapRef = useRef(null); // Initialize once on mount useEffect(() => { mapRef.current = new ThirdPartyMap(containerRef.current); return () => mapRef.current.destroy(); }, []); // Update on center change useEffect(() => { mapRef.current?.setCenter(center); }, [center]); return <div ref={containerRef} className="map" />; }
Synchronizing with External Store
useEffect(() => { document.title = `${notifications} notifications`; }, [notifications]); useEffect(() => { const mql = window.matchMedia('(prefers-color-scheme: dark)'); const handler = (e) => setDark(e.matches); mql.addEventListener('change', handler); setDark(mql.matches); return () => mql.removeEventListener('change', handler); }, []);
Cleanup — Why It Matters
The cleanup function runs: 1. Before the effect re-runs (when deps change). 2. When the component unmounts.
Without cleanup, you risk: - Memory leaks (lingering listeners, subscriptions) - State updates on unmounted components (log warnings in React 17, silent in 18) - Race conditions (stale fetch results writing over fresh ones)
useEffect(() => { let active = true; fetchUser(id).then(u => { if (active) setUser(u); }); return () => { active = false; }; // cancel stale update }, [id]);
Effect vs Event Handler — Which to Use?
| Trigger | Use |
|---|---|
| User interaction (click, submit) | Event handler |
| Synchronizing with an external system | useEffect |
| Computation derived from state/props | Neither — compute during render |
| Side effect triggered by state change you caused | Often better in the event handler |
// ANTI-PATTERN — using effect for user-triggered work function BadForm() { const [submitted, setSubmitted] = useState(false); useEffect(() => { if (submitted) sendToServer(); // delayed, hard to reason about }, [submitted]); return <button onClick={() => setSubmitted(true)}>Submit</button>; } // CORRECT — do it in the event handler function GoodForm() { const handleSubmit = () => sendToServer(); // synchronous, obvious return <button onClick={handleSubmit}>Submit</button>; }
Stale Closures and Fixes
// PROBLEM — stale closure: count is captured from the initial render useEffect(() => { const id = setInterval(() => { setCount(count + 1); // count is always 0 — stale! }, 1000); return () => clearInterval(id); }, []); // empty array = setup never re-runs // FIX A — functional update (doesn't need count in deps) useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []); // FIX B — add count to deps (interval re-created every tick) useEffect(() => { const id = setInterval(() => setCount(count + 1), 1000); return () => clearInterval(id); }, [count]); // FIX C — useRef to hold latest value const countRef = useRef(count); useEffect(() => { countRef.current = count; }); useEffect(() => { const id = setInterval(() => setCount(countRef.current + 1), 1000); return () => clearInterval(id); }, []);
useEffect with async/await
useEffect cannot be async. Wrap instead:
// WRONG useEffect(async () => { // returns a Promise, not a cleanup fn const data = await fetch('/api'); }, []); // CORRECT — async IIFE inside useEffect(() => { (async () => { const res = await fetch('/api/data'); const data = await res.json(); setData(data); })(); }, []); // CORRECT — named async function inside useEffect(() => { async function load() { const res = await fetch('/api/data'); const data = await res.json(); setData(data); } load(); }, []);
useLayoutEffect
Same signature as useEffect but fires synchronously after DOM mutations, before the browser paints. Use for measuring DOM or preventing visual flicker.
import { useLayoutEffect, useRef, useState } from 'react'; function Tooltip({ target }) { const ref = useRef(null); const [pos, setPos] = useState({ top: 0, left: 0 }); useLayoutEffect(() => { // Measure before paint — no flicker const rect = ref.current.getBoundingClientRect(); setPos({ top: rect.bottom, left: rect.left }); }, [target]); return <div ref={ref} style={pos}>Tooltip</div>; }
Prefer
useEffectby default. UseuseLayoutEffectonly when you observe flicker or need a DOM measurement that must happen before paint.