2.8 — useEffect Deep Dive: Quick Revision
Compact cheat sheet. Print-friendly.
How to use this material:
- Scan through before interviews or coding sessions.
- Use the tables as decision guides during development.
- If a concept feels fuzzy, revisit the full sub-topic file.
- Test yourself: cover the right column and recall from the left.
2.8.a — What useEffect Really Does
Mental Model
| Wrong Thinking | Correct Thinking |
|---|
| "When does this run?" | "What am I synchronising with?" |
| Mount / Update / Unmount | Start syncing / Stop syncing |
| Lifecycle replacement | External system synchronisation |
Three Kinds of Code
| Kind | Triggered By | Side Effects? | Example |
|---|
| Rendering code | React calling component | ❌ Must be pure | Calculate JSX |
| Event handler | User action | ✅ Yes | Click → send message |
| Effect | Rendering itself | ✅ Yes | Connect to WebSocket |
Execution Order
Render → Reconciliation → DOM commit → Browser paint → useEffect cleanup → useEffect setup
Children effects run before parent effects (bottom-up).
useEffect vs Event Handler
| Question | Answer |
|---|
| User clicked something? | Event handler |
| Component is displaying? | useEffect |
2.8.b — Dependency Array Behaviour
Three Configurations
| Config | Runs On | Syntax |
|---|
| No array | Every render | useEffect(() => {...}) |
Empty [] | Mount only | useEffect(() => {...}, []) |
| With deps | Mount + when deps change | useEffect(() => {...}, [a, b]) |
Object.is Comparison
| Type | Comparison | Safe in Deps? |
|---|
| Number, string, boolean | By value | ✅ Yes |
| null, undefined | Identity | ✅ Yes |
Object {} | By reference | ❌ New ref each render |
Array [] | By reference | ❌ New ref each render |
Function () => {} | By reference | ❌ New ref each render |
NaN vs NaN | true (unlike ===) | ✅ Yes |
Fix Strategies for Reference Types
| Strategy | When | Example |
|---|
| Move inside effect | Object only used in effect | Create {} inside useEffect |
| Destructure | Object from props | const { id } = user; [id] |
| useMemo | Expensive/shared object | useMemo(() => ({...}), [a]) |
| useCallback | Function dependency | useCallback(() => {...}, [a]) |
| JSON.stringify | Simple objects (last resort) | [JSON.stringify(config)] |
Infinite Loop Causes & Fixes
| Cause | Fix |
|---|
| Missing dep array | Add [] or [deps] |
| Object/array in deps | Move inside, destructure, useMemo |
| Function in deps | Move inside effect, useCallback |
| Setting state that's in deps | Updater function setState(prev => ...) |
2.8.c — Cleanup Functions
When Cleanup Runs
| Moment | What Happens |
|---|
| Dep change | Cleanup(old values) → Setup(new values) |
| Unmount | Cleanup(last values) |
Cleanup Pairs
| Setup | Cleanup |
|---|
addEventListener(type, fn) | removeEventListener(type, fn) |
setInterval(fn, ms) → id | clearInterval(id) |
setTimeout(fn, ms) → id | clearTimeout(id) |
new WebSocket(url) → ws | ws.close() |
new IntersectionObserver(fn) → obs | obs.disconnect() |
new ResizeObserver(fn) → obs | obs.disconnect() |
new Chart(ctx, cfg) → chart | chart.destroy() |
fetch(url, { signal }) | controller.abort() |
Key Rule
Cleanup is a closure — it sees the OLD render's values, not current. This is correct: you're cleaning up the previous synchronisation.
No Cleanup Needed
document.title = x
console.log(x)
analytics.log(x)
2.8.d — Data Fetching Pattern
Fetch State Pattern
{ status: 'idle' }
{ status: 'loading' }
{ status: 'success', data: ... }
{ status: 'error', error: '...' }
Race Condition Prevention
| Method | Cancels Request? | Prevents Stale State? |
|---|
| AbortController | ✅ Yes | ✅ Yes |
| Boolean flag | ❌ No | ✅ Yes |
| Both together | ✅ Yes | ✅ Yes |
Production Fetch Template
useEffect(() => {
const controller = new AbortController();
setState({ status: 'loading' });
fetch(url, { signal: controller.signal })
.then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); })
.then(data => setState({ status: 'success', data }))
.catch(err => { if (err.name !== 'AbortError') setState({ status: 'error', error: err.message }); });
return () => controller.abort();
}, [url]);
useEffect vs TanStack Query vs Server Components
| Feature | useEffect | TanStack Query | Server Components |
|---|
| Setup effort | High | Low | Low |
| Caching | Manual | Automatic | Framework |
| Race conditions | Manual | Automatic | N/A |
| Background refetch | ❌ | ✅ | ❌ |
| Use for | Learning, non-HTTP systems | Production client data | Initial page data |
2.8.e — Practical Example
Hook Composition
useDebouncedValue(query, 400) → debouncedQuery
↓
useFetch(`/api/search?q=${debouncedQuery}`) → { data, loading, error }
↓
useLocalStorageState('history', []) → [history, setHistory]
Key Patterns Used
| Pattern | Hook | Purpose |
|---|
| Debounce | useDebouncedValue | Prevent excessive API calls |
| Abort | useFetch (AbortController) | Cancel stale requests |
| Cache | useFetch (Map cache) | Avoid re-fetching same data |
| Persist | useLocalStorageState | Save search history |
| Cleanup | All hooks | Prevent memory leaks |
Core Formulas
useEffect = synchronise component with external system
Cleanup = undo what setup did
Dependencies = when to re-synchronise
Effect timing: render → DOM → paint → cleanup(old) → setup(new)
Layout timing: render → DOM → layoutEffect → paint
Object.is(a, b) ≈ a === b (except NaN===NaN → true, +0===-0 → false)
"Do I Need useEffect?" Decision
Transforming data? → Calculate during render / useMemo
Responding to user action? → Event handler
Initialising from props? → useState(prop) + key
Notifying parent? → Call callback in event handler
Caching calculation? → useMemo
Syncing with external? → ✅ useEffect
Last updated: April 2026