← back
Mirage / React / React Component Lifecycle
React
React
React Component Lifecycle
Mount → update → unmount with every hook explained. useEffect dependency patterns, common mistakes, hooks reference.
Three Phases
① Mount
Component created
function called for first time
render()
JSX evaluated, virtual DOM built
DOM updated
real DOM reflects virtual DOM
useEffect runs
[] dep array → runs once after mount
state/
props
change
② Update
State/props change
setState() or parent re-renders
re-render()
function runs again, new JSX
DOM diffing
React compares old vs new virtual DOM
Minimal DOM patch
only changed nodes updated in real DOM
useEffect runs
only if deps in [] changed
component
removed
③ Unmount
Component removed
navigated away or conditional removed it
cleanup runs
return () => {} in useEffect
DOM removed
nodes removed from real DOM
Memory freed
state, refs garbage collected
useEffect — when does it run?
Syntax When it runs Use for
useEffect(fn) after EVERY render rarely — usually causes infinite loops
useEffect(fn, []) once after mount only fetch initial data, set up listeners, init libs
useEffect(fn, [a, b]) after mount + when a or b changes re-fetch when userId changes, sync state
useEffect(fncleanup, []) cleanup runs on unmount remove event listeners, cancel timers, close connections
Effect execution order
Component mounts
first render complete
Effect runs
after paint (async)
Deps change
re-render triggered
Cleanup runs
before next effect
Effect runs again
fresh execution
⚠ Effects run after the browser paints. They are not synchronous. If you need to measure DOM before paint, use useLayoutEffect instead.
Core Hooks
useState
mount + update
stores a value that, when changed, re-renders the component
const [count, setCount] = useState(0)
const [user, setUser] = useState(null)

// update (always use setter)
setCount(prev => prev + 1)
useEffect
mount + update + unmount
run side effects after render — data fetching, subscriptions, timers
useEffect(() => {
  fetchGoals(userId)
  // cleanup on unmount:
  return () => cancel()
}, [userId]) // dep array
useRef
persists across renders
mutable value that doesn't trigger re-render. also used to access DOM elements directly.
// DOM ref
const inputRef = useRef(null)
<input ref={inputRef} />
inputRef.current.focus()

// mutable value
const count = useRef(0)
useMemo
update optimization
cache an expensive computed value. only recalculates when deps change.
const sorted = useMemo(
  () => [...goals].sort(fn),
  [goals] // only re-sort when goals changes
)
useCallback
update optimization
cache a function so it's not recreated every render. pass to child components.
const handleDelete = useCallback(
  (id) => deleteGoal(id),
  [] // stable — never recreated
)
useContext
global state
consume context value without prop drilling. any context change re-renders this component.
// in provider:
<AuthContext.Provider value={user}>

// in any child component:
const user = useContext(AuthContext)
Common Patterns
Data Fetching on Mount
fetch data once when component loads
const [data, setData] = useState(null)
const [err, setErr] = useState(null)

useEffect(() => {
  fetchGoals()
    .then(setData)
    .catch(setErr)
}, []) // [] = once on mount
Cleanup — Event Listener
remove listener when component unmounts
useEffect(() => {
  const handler = (e) => {
    if (e.key === 'Escape') close()
  }
  document.addEventListener('keydown', handler)
  // cleanup:
  return () => document.removeEventListener('keydown', handler)
}, [])
Re-fetch When ID Changes
watch a dependency and re-run effect
useEffect(() => {
  if (!userId) return
  fetchUserGoals(userId).then(setGoals)
}, [userId]) // re-runs when userId changes
Derived State (no effect needed)
calculate from existing state instead of syncing
// ❌ don't do this — unnecessary state
const [completed, setCompleted] = useState([])

// ✓ derive directly — always in sync
const completed = goals.filter(g => g.completed)
const pending = goals.filter(g => !g.completed)