React

React Life Cycle (Hooks era)

Component lifecycle in React 18+/19 with function components & hooks.

① Hooks Lifecycle Diagram — Function component (React 18+)

Recommendedfunction + useState / useEffect / useLayoutEffectReact ≥ 16.8 · 18 / 19

A visual map of the function component lifecycle in the hooks era. Arrows show the order React runs each phase.

Mounting (Mount)
Updating (Update)
Unmounting (Unmount)
Render phase
Pure — React may pause, abort or re-run it. No side effects.
↓ component is created
New propssetState()useReducer dispatch
↓ parent removes / key changes
Function body runs
useState(init)useReducer(reducer, init)useRef(init)useMemo / useCallback

→ returns JSX

Function body re-runs

Hooks run in the same order

useMemo / useCallback (deps)

→ React diffs the Virtual DOM

(skip render)
↓ React updates DOM and refs
Commit phase
Synchronous — the real DOM is updated, then effects / cleanups run.
useLayoutEffect
useLayoutEffect(fn, [])

Sync, before paint — measure DOM

old cleanup → new useLayoutEffect
useLayoutEffect(fn, [deps])
useLayoutEffect cleanup
return () => ...
🖼 Browser paint
useEffect
useEffect(fn, [])

Async, after paint — fetch / subscribe / timer

old cleanup → new useEffect
useEffect(fn, [deps])

When deps change value

useEffect cleanup
return () => ...

In reverse order — child first, parent last

⚠️In React 18+ Strict Mode (dev) a component will mount → unmount → mount once to force you to write cleanup correctly. Production runs normally.

Concurrent rendering (React 18+)

The render phase can be paused / aborted / re-run by React. Therefore:

  • Render must be pure — no side effects (don’t fetch or setState in the body).
  • Only the commit phase is guaranteed to run once and synchronously.
  • useTransition/useDeferredValue mark an update as interruptible.

② Class Lifecycle Diagram — Class component (legacy)

Legacy — reference onlyclass extends React.ComponentcomponentDidMount / Update / WillUnmount

A static diagram for class components using the same 3-column style so you can compare directly with the hooks diagram above.

MOUNTING
UPDATING
UNMOUNTING
constructor(props)

Init state, bind methods

static getDerivedStateFromProps

Sync state ⇐ props

componentWillUnmount()

Cancel timers, listeners, subscriptions

static getDerivedStateFromProps

Before the first render

shouldComponentUpdate(np, ns)

Return false to skip render

Component removed from the DOM

↓ render() — return JSX (pure)
render()

Output JSX

render()

Re-render with new state/props

↓ React commit DOM (pre-commit)
componentDidMount()

After DOM is mounted — fetch / subscribe

getSnapshotBeforeUpdate

Measure DOM before commit

componentDidUpdate(pp, ps)

After commit — side effects based on changes

💡Interactive diagram (class) — click each method for details:

React Lifecycle Methods diagram

Open in new tab

End-to-end example: Counter + Timer

The component below logs each phase to the console — run it to see the actual execution order between render, commit, useLayoutEffect and useEffect.

tsx
function Counter({ step }: { step: number }) { const [count, setCount] = useState(0); console.log('1. Render', { count, step }); useLayoutEffect(() => { console.log('3. useLayoutEffect (sync, before paint)'); return () => console.log(' ↳ cleanup useLayoutEffect'); }, [count]); useEffect(() => { console.log('4. useEffect (async, after paint)'); const id = setInterval(() => setCount((c) => c + step), 1000); return () => { console.log(' ↳ cleanup useEffect (interval)'); clearInterval(id); }; }, [step]); // 2. React commits the DOM here (between render and effect) return <p>Count: {count}</p>; }
Output on mount: 1 → 2 (commit) → 3 → paint → 4. When step changes: 1 → 2 → old useEffect cleanup → new 4 (useLayoutEffect does NOT re-run because its deps are [count]). On unmount: both cleanups run in reverse order.

Deps array — 3 forms & consequences

SyntaxWhen it runsUse forCommon bug
useEffect(fn)
no deps argument
After every renderAlmost neverInfinite loop if you setState inside
useEffect(fn, [])Once on mount, cleanup on unmountGlobal subscribe, init external libsStale closure — reads old state/props
useEffect(fn, [a, b])When a or b changes (Object.is compare)Sync with a specific prop / stateMissing deps → ESLint react-hooks/exhaustive-deps
💡Every value from the component scope (state, props, local functions) must be in the deps. If a function changes every render → wrap it with useCallback, or move it outside the component if it’s pure.

Common Pitfalls (5 frequent mistakes)

① Infinite loop

tsx
// ❌ setState every render → re-render → setState → ... useEffect(() => { setData({ ...data, ready: true }); }); // ✅ Add a deps array, or use a functional update useEffect(() => { setData((d) => ({ ...d, ready: true })); }, []);

② Stale closure

tsx
// ❌ count is always 0 — the closure captures the value at mount useEffect(() => { const id = setInterval(() => setCount(count + 1), 1000); return () => clearInterval(id); }, []); // ✅ functional update does not depend on the closure useEffect(() => { const id = setInterval(() => setCount((c) => c + 1), 1000); return () => clearInterval(id); }, []);

③ Forgetting cleanup

tsx
// ❌ Listener duplicated after every update / strict mode → memory leak useEffect(() => { window.addEventListener('resize', onResize); }, []); // ✅ Always return a cleanup useEffect(() => { window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []);

④ Async function directly

tsx
// ❌ useEffect doesn't accept a Promise — a returned Promise is ignored, cleanup won't run useEffect(async () => { const data = await fetch(url); }, [url]); // ✅ Declare async inside + a cancel flag useEffect(() => { let cancelled = false; (async () => { const data = await (await fetch(url)).json(); if (!cancelled) setData(data); })(); return () => { cancelled = true; }; }, [url]);

⑤ Using useEffect when you don’t need it (You Might Not Need an Effect)

tsx
// ❌ Deriving via an effect — extra render const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]); // ✅ Compute directly during render const fullName = `${first} ${last}`; // ❌ Reacting to an event handler inside an effect useEffect(() => { if (submitted) postData(form); }, [submitted]); // ✅ Put the logic in the event itself function handleSubmit() { postData(form); }

useLayoutEffect — when do you really need it?

useEffect runs after the browser paints, so the user may briefly see an "intermediate" frame before the effect updates the DOM. useLayoutEffect runs synchronously before paint → avoids flicker but blocks render.

tsx
// Tooltip needs to measure its size then position itself — useEffect would flicker function Tooltip({ targetRect, children }: Props) { const ref = useRef<HTMLDivElement>(null); const [top, setTop] = useState(0); useLayoutEffect(() => { const { height } = ref.current!.getBoundingClientRect(); setTop(targetRect.top - height - 8); // sync before paint → 0 flicker }, [targetRect]); return <div ref={ref} style={{ top }}>{children}</div>; }
Rule: use useEffect by default. Switch to useLayoutEffect only when the user sees flicker because you need to measure the DOM then mutate it immediately.

useLayoutEffect vs useEffect — comparison table

CriteriauseEffectuseLayoutEffect
TimingAfter paint (async)Before paint (sync)
Block UI?NoYes — slow work janks
SSROKWarning (runs on client only)
Use caseFetch, subscribe, log, timerMeasure DOM, scroll position, tooltip

Strict Mode double-invoke (React 18+)

In dev, React deliberately mounts → unmounts → mounts again once to force you to write cleanup correctly. Production is NOT affected.

tsx
useEffect(() => { console.log('connect'); const conn = chat.connect(); return () => { console.log('disconnect'); conn.disconnect(); }; }, []); // Dev console: // connect // disconnect ← React simulated unmount // connect ← mount again // → without cleanup, you'd have 2 connections alive at once

FAQ

Why does useEffect run twice right after I mount?

That’s Strict Mode in dev. Turning Strict Mode off (removing <StrictMode> in the layout) stops it, but the right fix is to write complete cleanup.

useEffect or event handler — when to use which?

If the logic is reacting to a user action (click, submit) → event handler. If it’s syncing with an external system (server, browser API, subscription) → useEffect.

When should you NOT use useEffect?
  • Computing a value from other state/props → compute it directly during render.
  • Resetting state when a prop changes → use key on the component.
  • Fetching on mount → use use() + Suspense / a framework data layer (Next.js, React Query).
  • Reacting to an event → put it in the handler.
What’s the execution order between parent & child?

Render: parent first, child after. Effects & cleanups: child first, parent last (bottom-up). useLayoutEffect follows the same rule, only running synchronously before paint.

React 19 — new lifecycle-related hooks

  • use(promise) — read a Promise/Context during render, integrated with Suspense. Replaces many useEffect + setState fetch cases.
  • useActionState(action, initial) — manage state for an async form action, with pending.
  • useFormStatus() — read the pending status of the parent form inside a submit button.
  • useOptimistic(state, reducer) — optimistic UI while an action is running.
  • Auto memoization (React Compiler) — will reduce the need for useMemo/useCallback.

useEffect — pattern cheatsheet

tsx
useEffect(() => { // Mount + whenever deps change (Update) const id = setInterval(tick, 1000); return () => clearInterval(id); // Cleanup: runs on Unmount OR before the next effect }, [deps]);

General render flow

Mount:
  1. Initialize state (useState/useReducer)
  2. Render JSX
  3. Commit to the DOM
  4. useLayoutEffect (sync)
  5. Paint
  6. useEffect (async)

Update:
  1. State/props change → re-render
  2. Diff & commit
  3. Old effect cleanup → new useEffect (if deps changed)

Unmount:
  1. Clean up all effects (child first, parent last)

Common hooks

tsx
useState(initial) // simple state useReducer(reducer, init) // complex state / many actions useRef(initial) // value box, no re-render useMemo(fn, [deps]) // cache an expensive compute useCallback(fn, [deps]) // cache a function reference useTransition() // mark a non-urgent update useDeferredValue(value) // defer a value until idle useId() // stable id for a11y useSyncExternalStore() // subscribe to a store outside React

Class → Hooks mapping

Class methodHook equivalentNotes
componentDidMountuseEffect(fn, [])Strict Mode dev runs it twice
componentDidUpdateuseEffect(fn, [deps])Declare the right deps
componentWillUnmountuseEffect(() => () => {...}, [])Return a cleanup function
shouldComponentUpdateReact.memo + useMemoOr let the React Compiler handle it
getDerivedStateFromPropsCompute directly during renderOr reset with key
getSnapshotBeforeUpdateuseLayoutEffectMeasure DOM before paint
componentDidCatchError Boundary (still needs a class)Or react-error-boundary
this.state / setStateuseState / useReducersetState merges objects → useState replaces
this.refs / createRefuseRefMutating .current does not re-render
Chào bạn, mình là Nova. Có phần nào bạn muốn hiểu kỹ hơn không? Cứ nói với mình nhé.