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+)
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.
useState(init)useReducer(reducer, init)useRef(init)useMemo / useCallback→ trả về JSX
Hooks chạy theo cùng thứ tự
useMemo / useCallback (deps)→ React diff Virtual DOM
useLayoutEffect(fn, [])Sync, trước paint — đo DOM
useLayoutEffect(fn, [deps])return () => ...useEffect(fn, [])Async, sau paint — fetch / subscribe / timer
useEffect(fn, [deps])Khi deps đổi value
return () => ...Theo thứ tự ngược — child trước, parent sau
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
fetchhaysetStatetrong 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)
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.
constructor(props)Init state, bind methods
static getDerivedStateFromPropsSync state ⇐ props
componentWillUnmount()Huỷ timer, listener, subscription
static getDerivedStateFromPropsTrước render lần đầu
shouldComponentUpdate(np, ns)Trả false để skip render
Component bị gỡ khỏi DOM
render()Output JSX
render()Re-render với state/props mới
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
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.
tsxfunction 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>; }
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áp | Khi nào chạy | Dùng cho | Bug thường gặp |
|---|---|---|---|
useEffect(fn)không truyền deps | Sau mỗi render | Hầu như không bao giờ | Infinite loop nếu setState bên trong |
useEffect(fn, []) | 1 lần khi mount, cleanup khi unmount | Subscribe global, init lib bên ngoài | Stale 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 |
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>; }
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í | useEffect | useLayoutEffect |
|---|---|---|
| Thời điểm | Sau paint (async) | Trước paint (sync) |
| Block UI? | Không | Có — chậm sẽ giật |
| SSR | OK | Cảnh báo (chỉ chạy ở client) |
| Use case | Fetch, 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ị.
tsxuseEffect(() => { 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
keytrê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 caseuseEffect + 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
tsxuseEffect(() => { // 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
tsxuseState(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 method | Hook tương đương | Ghi chú |
|---|---|---|
componentDidMount | useEffect(fn, []) | Strict Mode dev sẽ chạy 2 lần |
componentDidUpdate | useEffect(fn, [deps]) | Khai báo đúng deps |
componentWillUnmount | useEffect(() => () => {...}, []) | Return cleanup function |
shouldComponentUpdate | React.memo + useMemo | Hoặc React Compiler tự lo |
getDerivedStateFromProps | Tính trực tiếp khi render | Hoặc reset bằng key |
getSnapshotBeforeUpdate | useLayoutEffect | Đo DOM trước paint |
componentDidCatch | Error Boundary (vẫn cần class) | Hoặc react-error-boundary |
this.state / setState | useState / useReducer | setState merge object → useState replace |
this.refs / createRef | useRef | Mutate .current không re-render |