React

React Life Cycle (Hooks era)

Vòng đời component trong React 18+/19 với function component & hooks.

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

✅ Khuyên dùngfunction + useState / useEffect / useLayoutEffectReact ≥ 16.8 · 18 / 19

Sơ đồ trực quan vòng đời function component thời hooks. Mũi tên thể hiện thứ tự React thực thi giữa các pha.

Quá trình gắn kết (Mount)
Quá trình cập nhật (Update)
Quá trình tách rời (Unmount)
Giai đoạn Render
Pure — có thể bị React tạm dừng, huỷ hoặc chạy lại. Không side-effect.
↓ component được tạo
New propssetState()useReducer dispatch
↓ parent gỡ / key đổi
Function body chạy
useState(init)useReducer(reducer, init)useRef(init)useMemo / useCallback

→ trả về JSX

Function body chạy lại

Hooks chạy theo cùng thứ tự

useMemo / useCallback (deps)

→ React diff Virtual DOM

(skip render)
↓ React updates DOM and refs
Giai đoạn Commit
Đồng bộ — DOM thật được cập nhật, sau đó chạy effect / cleanup.
useLayoutEffect
useLayoutEffect(fn, [])

Sync, trước paint — đo DOM

cleanup cũ → useLayoutEffect mới
useLayoutEffect(fn, [deps])
cleanup useLayoutEffect
return () => ...
🖼 Browser paint
useEffect
useEffect(fn, [])

Async, sau paint — fetch / subscribe / timer

cleanup cũ → useEffect mới
useEffect(fn, [deps])

Khi deps đổi value

cleanup useEffect
return () => ...

Theo thứ tự ngược — child trước, parent sau

⚠️React 18+ ở Strict Mode (dev) sẽ mount → unmount → mount 1 lần để buộc bạn viết cleanup đúng. Production chạy bình thường.

Concurrent rendering (React 18+)

Render phase có thể bị React tạm dừng / huỷ / chạy lại. Vì vậy:

  • Render phải pure — không side-effect (đừng fetch hay setState trong body).
  • Chỉ commit phase mới chắc chắn chạy 1 lần và đồng bộ.
  • useTransition/useDeferredValue đánh dấu update có thể bị gián đoạn.

② Class Lifecycle Diagram — Class component (legacy)

⚠ Legacy — chỉ tham khảoclass extends React.ComponentcomponentDidMount / Update / WillUnmount

Sơ đồ tĩnh cho class component với cùng style 3 cột để bạn so sánh trực tiếp với diagram hooks ở trên.

MOUNTING
UPDATING
UNMOUNTING
constructor(props)

Init state, bind methods

static getDerivedStateFromProps

Sync state ⇐ props

componentWillUnmount()

Huỷ timer, listener, subscription

static getDerivedStateFromProps

Trước render lần đầu

shouldComponentUpdate(np, ns)

Trả false để skip render

Component bị gỡ khỏi DOM

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

Output JSX

render()

Re-render với state/props mới

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

Sau khi DOM gắn — fetch / subscribe

getSnapshotBeforeUpdate

Đo DOM trước khi commit

componentDidUpdate(pp, ps)

Sau commit — side-effect dựa trên thay đổi

💡Diagram tương tác (class) — click vào từng method để xem chi tiết:

Ví dụ end-to-end: Counter + Timer

Component dưới đây log từng giai đoạn ra console — chạy thử để thấy thứ tự thực thi thực tế giữa render, commit, useLayoutEffect và useEffect.

tsx
function Counter({ step }: { step: number }) { const [count, setCount] = useState(0); console.log('1. Render', { count, step }); useLayoutEffect(() => { console.log('3. useLayoutEffect (sync, trước paint)'); return () => console.log(' ↳ cleanup useLayoutEffect'); }, [count]); useEffect(() => { console.log('4. useEffect (async, sau paint)'); const id = setInterval(() => setCount((c) => c + step), 1000); return () => { console.log(' ↳ cleanup useEffect (interval)'); clearInterval(id); }; }, [step]); // 2. React commit DOM ở đây (giữa render và effect) return <p>Count: {count}</p>; }
Output khi mount: 1 → 2 (commit) → 3 → paint → 4. Khi step đổi: 1 → 2 → cleanup useEffect cũ → 4 mới (useLayoutEffect KHÔNG re-run vì deps là [count]). Khi unmount: cả 2 cleanup chạy theo thứ tự ngược.

Deps array — 3 dạng & hậu quả

Cú phápKhi nào chạyDùng choBug thường gặp
useEffect(fn)
không truyền deps
Sau mỗi renderHầu như không bao giờInfinite loop nếu setState bên trong
useEffect(fn, [])1 lần khi mount, cleanup khi unmountSubscribe global, init lib bên ngoàiStale closure — đọc state/props cũ
useEffect(fn, [a, b])Khi a hoặc b đổi (so sánh Object.is)Đồng bộ với prop / state cụ thểQuên deps → ESLint react-hooks/exhaustive-deps
💡Mọi giá trị từ scope component (state, props, function local) phải có trong deps. Nếu function thay đổi mỗi render → bọc bằng useCallback, hoặc move ra ngoài component nếu thuần.

Common Pitfalls (5 lỗi hay gặp)

① Infinite loop

tsx
// ❌ setState mỗi render → render lại → setState → ... useEffect(() => { setData({ ...data, ready: true }); }); // ✅ Có deps array, hoặc dùng functional update useEffect(() => { setData((d) => ({ ...d, ready: true })); }, []);

② Stale closure

tsx
// ❌ count luôn = 0 vì closure bắt giá trị lúc mount useEffect(() => { const id = setInterval(() => setCount(count + 1), 1000); return () => clearInterval(id); }, []); // ✅ functional update không phụ thuộc closure useEffect(() => { const id = setInterval(() => setCount((c) => c + 1), 1000); return () => clearInterval(id); }, []);

③ Quên cleanup

tsx
// ❌ Listener bị duplicate sau mỗi update / strict mode → memory leak useEffect(() => { window.addEventListener('resize', onResize); }, []); // ✅ Luôn return cleanup useEffect(() => { window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []);

④ Async function trực tiếp

tsx
// ❌ useEffect không nhận Promise — trả về Promise sẽ bị bỏ qua, cleanup không chạy useEffect(async () => { const data = await fetch(url); }, [url]); // ✅ Khai báo async bên trong + cờ huỷ useEffect(() => { let cancelled = false; (async () => { const data = await (await fetch(url)).json(); if (!cancelled) setData(data); })(); return () => { cancelled = true; }; }, [url]);

⑤ Dùng useEffect khi không cần (You Might Not Need an Effect)

tsx
// ❌ Derive bằng effect — render thừa const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]); // ✅ Tính trực tiếp khi render const fullName = `${first} ${last}`; // ❌ Phản ứng event handler trong effect useEffect(() => { if (submitted) postData(form); }, [submitted]); // ✅ Đặt logic ngay tại event function handleSubmit() { postData(form); }

useLayoutEffect — khi nào thực sự cần?

useEffect chạy sau khi browser paint nên user có thể nhìn thấy 1 frame "trung gian" trước khi effect cập nhật DOM. useLayoutEffect chạy đồng bộ trước paint → tránh nhấp nháy nhưng chặn render.

tsx
// Tooltip cần đo kích thước rồi tự định vị — useEffect sẽ nhấp nháy 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 trước paint → 0 nhấp nháy }, [targetRect]); return <div ref={ref} style={{ top }}>{children}</div>; }
Quy tắc: dùng useEffect mặc định. Chỉ đổi sang useLayoutEffect khi user thấy nhấp nháy do bạn cần đo DOM rồi mutate ngay.

useLayoutEffect vs useEffect — bảng so sánh

Tiêu chíuseEffectuseLayoutEffect
Thời điểmSau paint (async)Trước paint (sync)
Block UI?KhôngCó — chậm sẽ giật
SSROKCảnh báo (chỉ chạy ở client)
Use caseFetch, subscribe, log, timerĐo DOM, scroll position, tooltip

Strict Mode double-invoke (React 18+)

Trong dev, React cố tình mount → unmount → mount lại 1 lần để buộc bạn viết cleanup đúng. Production KHÔNG bị.

tsx
useEffect(() => { console.log('connect'); const conn = chat.connect(); return () => { console.log('disconnect'); conn.disconnect(); }; }, []); // Dev console: // connect // disconnect ← React unmount giả lập // connect ← mount lại // → nếu thiếu cleanup, bạn sẽ có 2 connection sống song song

FAQ

Tại sao useEffect chạy 2 lần khi tôi mới mount?

Đó là Strict Mode trong dev. Tắt Strict Mode (gỡ <StrictMode> trong layout) sẽ hết, nhưng cách đúng là viết cleanup đầy đủ.

useEffect hay event handler — khi nào dùng cái nào?

Nếu logic là phản ứng với hành động user (click, submit) → event handler. Nếu là đồng bộ với hệ thống bên ngoài (server, browser API, subscription) → useEffect.

Khi nào KHÔNG nên dùng useEffect?
  • Tính giá trị từ state/props khác → tính trực tiếp khi render.
  • Reset state khi prop đổi → dùng key trên component.
  • Fetch khi mount → dùng use() + Suspense / framework data layer (Next.js, React Query).
  • Phản ứng event → đặt vào handler.
Thứ tự chạy giữa parent & child?

Render: parent trước, child sau. Effect & cleanup: child trước, parent sau (bottom-up). useLayoutEffect cùng quy tắc, chỉ khác là sync trước paint.

React 19 — Hooks mới liên quan lifecycle

  • use(promise) — đọc Promise/Context trong render, tích hợp Suspense. Thay nhiều case useEffect + setState để fetch.
  • useActionState(action, initial) — quản lý state cho form action async, kèm pending.
  • useFormStatus() — đọc trạng thái pending của form cha trong button submit.
  • useOptimistic(state, reducer) — UI lạc quan trong khi action đang chạy.
  • Auto memoization (React Compiler) — sẽ giảm nhu cầu useMemo/useCallback.

useEffect — pattern cheatsheet

tsx
useEffect(() => { // Mount + mỗi khi deps thay đổi (Update) const id = setInterval(tick, 1000); return () => clearInterval(id); // Cleanup: chạy khi Unmount HOẶC trước lần effect mới }, [deps]);

Render flow tổng quát

Mount:
  1. Khởi tạo state (useState/useReducer)
  2. Render JSX
  3. Commit vào DOM
  4. useLayoutEffect (sync)
  5. Paint
  6. useEffect (async)

Update:
  1. State/props thay đổi → re-render
  2. Diff & commit
  3. Cleanup effect cũ → useEffect mới (nếu deps đổi)

Unmount:
  1. Cleanup tất cả effect (child trước, parent sau)

Common hooks

tsx
useState(initial) // state đơn giản useReducer(reducer, init) // state phức tạp / nhiều action useRef(initial) // box giá trị, không re-render useMemo(fn, [deps]) // cache compute đắt useCallback(fn, [deps]) // cache function reference useTransition() // đánh dấu update không cấp bách useDeferredValue(value) // hoãn 1 giá trị tới khi rảnh useId() // id ổn định cho a11y useSyncExternalStore() // đăng ký store ngoài React

Class → Hooks mapping

Class methodHook tương đươngGhi chú
componentDidMountuseEffect(fn, [])Strict Mode dev sẽ chạy 2 lần
componentDidUpdateuseEffect(fn, [deps])Khai báo đúng deps
componentWillUnmountuseEffect(() => () => {...}, [])Return cleanup function
shouldComponentUpdateReact.memo + useMemoHoặc React Compiler tự lo
getDerivedStateFromPropsTính trực tiếp khi renderHoặc reset bằng key
getSnapshotBeforeUpdateuseLayoutEffectĐo DOM trước paint
componentDidCatchError Boundary (vẫn cần class)Hoặc react-error-boundary
this.state / setStateuseState / useReducersetState merge object → useState replace
this.refs / createRefuseRefMutate .current không re-render