Mục lục
Xem mục lục
Lifecycle là gì
Lifecycle (vòng đời) là các giai đoạn một component React trải qua từ lúc được tạo ra trên DOM, cập nhật khi state/props thay đổi, cho tới lúc bị gỡ bỏ. Có 3 giai đoạn chính:
- Mounting: component lần đầu được tạo ra và chèn vào DOM.
- Updating: component render lại do state hoặc props thay đổi.
- Unmounting: component bị loại khỏi DOM.
Class component có các lifecycle method riêng cho từng giai đoạn, gọi tự động theo đúng thứ tự. Function component không có các method riêng biệt, dùng useEffect để mô phỏng lại cả 3 giai đoạn trong cùng một hook.
Mounting: component được tạo ra
Class component, thứ tự gọi:
constructor → render → componentDidMount
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
console.log("Component đã được chèn vào DOM");
}
render() {
return <p>{this.state.count}</p>;
}
}
Function component, dùng useEffect với dependency array rỗng:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Component đã được chèn vào DOM");
}, []);
return <p>{count}</p>;
}
Dependency array rỗng ([]) báo cho React biết effect này chỉ cần chạy một lần, ngay sau lần render đầu tiên, tương đương componentDidMount.
Updating: component render lại
Class component:
render → componentDidUpdate
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log("count đã đổi thành", this.state.count);
}
}
componentDidUpdate nhận prevProps/prevState để so sánh thủ công, tự kiểm tra xem giá trị nào đã thay đổi.
Function component, dùng useEffect với dependency cụ thể:
useEffect(() => {
console.log("count đã đổi thành", count);
}, [count]);
React tự so sánh giá trị trong dependency array giữa các lần render, chỉ chạy lại effect khi giá trị đó thay đổi, không cần tự viết logic so sánh như componentDidUpdate.
Unmounting: component bị gỡ bỏ
Class component:
componentWillUnmount() {
console.log("Component sắp bị gỡ khỏi DOM");
}
Function component, dùng cleanup function trả về từ useEffect:
useEffect(() => {
console.log("Component đã mount");
return () => {
console.log("Component sắp bị gỡ khỏi DOM");
};
}, []);
Function trả về từ useEffect chạy vào hai thời điểm: ngay trước khi component unmount, hoặc ngay trước khi effect chạy lại ở lần tiếp theo (nếu dependency thay đổi). Đây là điểm khác biệt so với componentWillUnmount, vốn chỉ gọi đúng một lần khi unmount.
Bảng đối chiếu class lifecycle method và useEffect
| Giai đoạn | Class component | Function component |
|---|---|---|
| Mounting | componentDidMount | useEffect(() => {...}, []) |
| Updating | componentDidUpdate(prevProps, prevState) | useEffect(() => {...}, [dependency]) |
| Unmounting | componentWillUnmount | Cleanup function: return () => {...} trong useEffect |
| So sánh giá trị cũ/mới | Tự so sánh thủ công qua prevProps/prevState | React tự so sánh dependency array |
Vì sao dễ gây memory leak nếu quên cleanup
Một effect đăng ký thứ gì đó tồn tại lâu dài (subscription, timer, event listener) nhưng không dọn dẹp khi unmount sẽ khiến thứ đó tiếp tục tồn tại, dù component đã biến mất khỏi DOM:
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
// Thiếu cleanup: interval vẫn chạy mãi kể cả sau khi component unmount
}, []);
Cách sửa: luôn trả về function dọn dẹp tương ứng với thứ đã đăng ký:
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(timer);
}, []);
Nguyên tắc chung: mọi effect đăng ký thứ gì đó (subscription, timer, event listener, WebSocket…) đều nên có cleanup tương ứng, trừ khi thứ đó tự kết thúc ngay trong effect (như một lần fetch dữ liệu không cần hủy).
Cạm bẫy: set state sau khi component đã unmount
Gọi setState/setter của useState sau khi component đã unmount gây cảnh báo quen thuộc: “Can’t perform a React state update on an unmounted component”. Tình huống điển hình: fetch dữ liệu trong effect, nhưng component unmount trước khi request kịp trả về:
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data); // Nếu component đã unmount trước khi fetch xong, gây cảnh báo
});
}, [userId]);
Cách tránh phổ biến nhất: dùng AbortController để hủy request khi unmount, đồng thời tránh luôn việc gọi setState sau khi đã hủy:
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(data => setUser(data))
.catch(err => {
if (err.name !== "AbortError") throw err;
});
return () => controller.abort();
}, [userId]);
Cách này vừa tránh warning, vừa tránh lãng phí: request không còn cần thiết được hủy thật sự ở tầng network, không chỉ đơn thuần bỏ qua kết quả.
Tổng kết
- Lifecycle có 3 giai đoạn: Mounting (tạo ra), Updating (cập nhật), Unmounting (gỡ bỏ).
- Class component dùng các lifecycle method riêng:
componentDidMount,componentDidUpdate,componentWillUnmount. - Function component dùng
useEffectđể mô phỏng cả 3 giai đoạn, dependency array quyết định effect chạy khi nào, cleanup function đảm nhiệm phần unmount. - Quên cleanup subscription/timer/event listener là nguồn memory leak phổ biến, luôn trả về function dọn dẹp tương ứng với thứ đã đăng ký trong effect.
- Set state sau khi unmount gây cảnh báo, tránh bằng cách hủy thao tác bất đồng bộ (như
AbortController) trong cleanup function.