Episode 2 — React Frontend Architecture NextJS / 2.8 — useEffect Deep Dive

2.8 — useEffect Deep Dive: Quick Revision

Compact cheat sheet. Print-friendly.

How to use this material:

  1. Scan through before interviews or coding sessions.
  2. Use the tables as decision guides during development.
  3. If a concept feels fuzzy, revisit the full sub-topic file.
  4. Test yourself: cover the right column and recall from the left.

2.8.a — What useEffect Really Does

Mental Model

Wrong ThinkingCorrect Thinking
"When does this run?""What am I synchronising with?"
Mount / Update / UnmountStart syncing / Stop syncing
Lifecycle replacementExternal system synchronisation

Three Kinds of Code

KindTriggered BySide Effects?Example
Rendering codeReact calling component❌ Must be pureCalculate JSX
Event handlerUser action✅ YesClick → send message
EffectRendering itself✅ YesConnect 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

QuestionAnswer
User clicked something?Event handler
Component is displaying?useEffect

2.8.b — Dependency Array Behaviour

Three Configurations

ConfigRuns OnSyntax
No arrayEvery renderuseEffect(() => {...})
Empty []Mount onlyuseEffect(() => {...}, [])
With depsMount + when deps changeuseEffect(() => {...}, [a, b])

Object.is Comparison

TypeComparisonSafe in Deps?
Number, string, booleanBy value✅ Yes
null, undefinedIdentity✅ Yes
Object {}By reference❌ New ref each render
Array []By reference❌ New ref each render
Function () => {}By reference❌ New ref each render
NaN vs NaNtrue (unlike ===)✅ Yes

Fix Strategies for Reference Types

StrategyWhenExample
Move inside effectObject only used in effectCreate {} inside useEffect
DestructureObject from propsconst { id } = user; [id]
useMemoExpensive/shared objectuseMemo(() => ({...}), [a])
useCallbackFunction dependencyuseCallback(() => {...}, [a])
JSON.stringifySimple objects (last resort)[JSON.stringify(config)]

Infinite Loop Causes & Fixes

CauseFix
Missing dep arrayAdd [] or [deps]
Object/array in depsMove inside, destructure, useMemo
Function in depsMove inside effect, useCallback
Setting state that's in depsUpdater function setState(prev => ...)

2.8.c — Cleanup Functions

When Cleanup Runs

MomentWhat Happens
Dep changeCleanup(old values) → Setup(new values)
UnmountCleanup(last values)

Cleanup Pairs

SetupCleanup
addEventListener(type, fn)removeEventListener(type, fn)
setInterval(fn, ms) → idclearInterval(id)
setTimeout(fn, ms) → idclearTimeout(id)
new WebSocket(url) → wsws.close()
new IntersectionObserver(fn) → obsobs.disconnect()
new ResizeObserver(fn) → obsobs.disconnect()
new Chart(ctx, cfg) → chartchart.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

// Discriminated union (recommended)
{ status: 'idle' }
{ status: 'loading' }
{ status: 'success', data: ... }
{ status: 'error', error: '...' }

Race Condition Prevention

MethodCancels 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

FeatureuseEffectTanStack QueryServer Components
Setup effortHighLowLow
CachingManualAutomaticFramework
Race conditionsManualAutomaticN/A
Background refetch
Use forLearning, non-HTTP systemsProduction client dataInitial 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

PatternHookPurpose
DebounceuseDebouncedValuePrevent excessive API calls
AbortuseFetch (AbortController)Cancel stale requests
CacheuseFetch (Map cache)Avoid re-fetching same data
PersistuseLocalStorageStateSave search history
CleanupAll hooksPrevent 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