Vòng đời component React có ba giai đoạn: mount (component xuất hiện trên màn hình lần đầu), update (component render lại vì state, props hoặc context thay đổi) và unmount (component bị gỡ bỏ). Trong function component, bạn phản ứng với các giai đoạn này bằng useEffect: effect với [] chạy sau khi mount, effect có dependency chạy sau những lần update mà dependency thay đổi, còn hàm do effect trả về chạy khi unmount.
Console bên dưới phần xem trước hiển thị từng giai đoạn khi nó diễn ra.
Console bắt đầu với render, count = 0 và mounted. Mỗi lần bấm Count sẽ ghi log một lần render và một lần update. Unmount ghi unmounted; Mount lại sẽ bắt đầu từ đầu với count = 0, vì state đã bị vứt bỏ cùng component.
Mount
Một component được mount lần đầu tiên React render nó tại một vị trí nhất định trong cây. React gọi hàm, tạo các nút DOM từ JSX mà hàm trả về, chèn chúng vào, để trình duyệt vẽ, rồi chạy các effect của component.
useEffect(fn, []) là nơi bạn khởi động những thứ cần tồn tại suốt thời gian component còn trên màn hình: một subscription, một timer, một kết nối, request dữ liệu đầu tiên. Nó chạy sau khi vẽ, nên không bao giờ làm chậm khung hình đầu tiên.
Update
Một component update (render lại) khi:
- state của chính nó thay đổi,
- component cha render lại (mặc định, mọi component con cũng render theo, dù props của chúng có đổi hay không),
- một context mà nó đọc thay đổi.
Sau mỗi lần update, React chạy những effect có dependency thay đổi. Trước khi chạy lại một effect, React chạy cleanup trước đó của effect ấy, nên subscription cũ được đóng trước khi subscription mới mở ra. Trang rendering trình bày chi tiết hơn những gì kích hoạt một lần render.
Unmount
Một component bị unmount khi component cha thôi render nó (như {show && <Counter />} ở trên), khi key của nó thay đổi, hoặc khi chính root bị unmount. React gỡ các nút DOM của nó, chạy cleanup của mọi effect và bỏ đi state. Nếu sau này component xuất hiện lại, đó là một instance hoàn toàn mới.
Component cha và con
React render từ trên xuống nhưng chạy effect từ dưới lên. Khi cha và con mount cùng lúc, effect của con chạy trước, nên effect của cha có thể tin rằng các con đã được thiết lập xong.
Khi mount, log ghi render Parent, render Child, Child effect, Parent effect. Bấm Remove, các cleanup chạy theo chiều ngược lại: Parent cleanup, rồi Child cleanup.
Pha render và pha commit
Mỗi lần update đi qua hai pha:
- Pha render. React gọi các component của bạn để tính JSX mới và so sánh với kết quả trước. Chưa có gì trên màn hình thay đổi. React có thể tạm dừng phần việc này, chạy nó hai lần trong StrictMode, hoặc bỏ đi, vì vậy việc render phải thuần: không request, không subscription, không thay đổi biến bên ngoài.
- Pha commit. React áp dụng các khác biệt vào DOM, gắn ref và chạy các callback
useLayoutEffect, tất cả trước khi trình duyệt vẽ. Các callbackuseEffectchạy sau đó, thường là khi trình duyệt đã vẽ xong. Khi lần update do một cú click hoặc một thao tác nhập rời rạc khác gây ra, React chạy chúng ngay sau layout effect, để kết quả sẵn sàng trước sự kiện tiếp theo.
Mỗi lần bấm đều ghi ba dòng theo cùng một thứ tự. Các effect này không có mảng dependency, nên chạy sau mỗi lần render. Thêm [] làm đối số thứ hai cho cả hai, chúng sẽ chỉ chạy sau lần render đầu tiên.
Phương thức vòng đời của class và hook tương đương
Class component viết vòng đời ra thành các phương thức có tên. Bạn sẽ gặp chúng trong code cũ và trong error boundary, thứ vẫn cần một class.
| Phương thức class | Khi nào chạy | Tương đương trong function component |
|---|---|---|
constructor | một lần, trước lần render đầu tiên | useState(initial), useRef |
static getDerivedStateFromProps | trước mỗi lần render | tính giá trị ngay khi render, hoặc đặt lại bằng key |
render | mỗi lần render | thân hàm |
componentDidMount | sau khi lần render đầu tiên đã vào DOM | useEffect(fn, []) |
shouldComponentUpdate | trước một lần update, có thể bỏ qua nó | React.memo (nó trả lời câu hỏi ngược lại: "props có bằng nhau không") |
getSnapshotBeforeUpdate | ngay trước khi DOM thay đổi | không có hook trực tiếp; gần nhất là useLayoutEffect cùng một ref |
componentDidUpdate | sau mỗi lần update | useEffect(fn, [deps]) (cũng chạy sau khi mount) |
componentWillUnmount | trước khi bị gỡ bỏ | cleanup do useEffect trả về |
getDerivedStateFromError, componentDidCatch | khi một component con ném lỗi | không có: hãy viết một class error boundary |
Đây là một class ghi log từng phương thức khi nó chạy:
Khi mount, log ghi constructor, getDerivedStateFromProps, render, componentDidMount. Một cú bấm ghi getDerivedStateFromProps, shouldComponentUpdate, render, getSnapshotBeforeUpdate, componentDidUpdate. Cho shouldComponentUpdate trả về false, các cú bấm sẽ không còn cập nhật nút nữa: state thay đổi, nhưng React bỏ qua lần render. Trang class component chỉ cách chuyển một class như thế này sang hàm.
Nghĩ theo đồng bộ hóa, không theo vòng đời
Bảng tương đương hook rất hữu ích khi đọc code cũ, nhưng nó có thể khiến bạn viết effect theo thói quen của class, như một effect "did mount" lớn và một effect "did update" lớn. Tốt hơn hãy hiểu một effect là "giữ thứ bên ngoài này đồng bộ với những giá trị này": kết nối tới roomId, và khi roomId thay đổi thì ngắt kết nối rồi kết nối lại. Hãy viết một effect cho mỗi thứ bạn đồng bộ, liệt kê các giá trị nó dùng làm dependency, và để React quyết định khi nào chạy nó.
Câu hỏi thường gặp
Vòng đời component React gồm những giai đoạn nào?
Mount (React tạo component và đưa đầu ra của nó lên màn hình), update (component render lại vì state, props hoặc context thay đổi) và unmount (component bị gỡ bỏ và dọn dẹp).
Hook nào tương đương với componentDidMount?
useEffect(() => { ... }, []). Mảng dependency rỗng khiến nó chạy một lần, sau khi lần render đầu tiên đã lên màn hình. Hàm mà nó trả về tương đương với componentWillUnmount.
Hook nào tương đương với componentDidUpdate?
useEffect với các giá trị bạn quan tâm trong mảng dependency, chẳng hạn [userId]. Khác với componentDidUpdate, nó cũng chạy sau lần render đầu tiên, nên bao gồm cả mount lẫn update.
Effect của component cha và con chạy theo thứ tự nào?
React render từ cha xuống, nhưng chạy effect từ con lên: effect của component con chạy trước effect của cha. Khi unmount, cleanup của cha chạy trước cleanup của con.
Pha render và pha commit là gì?
Trong pha render, React gọi các component của bạn để tính xem màn hình nên trông thế nào; nó có thể tạm dừng hoặc bỏ đi phần việc này. Trong pha commit, React áp dụng các thay đổi vào DOM và chạy layout effect; effect thông thường chạy sau đó, thường là khi trình duyệt đã vẽ xong.