Lỗi Gặp Phải
Tab trình duyệt của bạn bắt đầu chậm dần, rồi console tràn ngập cùng một dòng lặp đi lặp lại hàng trăm lần:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
React đã chạm đến giới hạn re-render nội bộ — thường khoảng 50 lần cập nhật lồng nhau — và ném ra cảnh báo này để ngăn trình duyệt bị crash hoàn toàn. Component đang mắc kẹt trong một vòng lặp vô tận.
Nguyên Nhân
Vòng lặp có cấu trúc đơn giản đến tàn nhẫn:
- Component render
useEffectchạy và gọisetState- State thay đổi kích hoạt re-render
- Re-render khiến
useEffectchạy lại - Quay về bước 2 — mãi mãi
Ba nguyên nhân gốc rễ sau chiếm 95% các trường hợp thực tế:
- Thiếu dependency array — không có array đồng nghĩa với việc effect chạy sau mỗi lần render, không có ngoại lệ
- Object hoặc array trong dependency array — một reference mới được tạo ra mỗi lần render, nên phép so sánh nông của React luôn nhận thấy sự thay đổi
- Cập nhật state vô điều kiện bên trong effect — dù dependency array có hoàn hảo đến đâu cũng không cứu được bạn nếu bạn luôn gọi
setState
Cách Sửa Theo Từng Trường Hợp
Trường Hợp 1: Thiếu Dependency Array
Không có array là lỗi phổ biến nhất với người mới học. Effect sẽ coi mỗi lần render là một lần kích hoạt.
// ❌ Lỗi — chạy sau mỗi lần render
useEffect(() => {
setCount(count + 1);
});
// ✅ Đã sửa — chỉ chạy một lần khi mount
useEffect(() => {
setCount(1);
}, []);
[] rỗng đó không phải tùy chọn — đó là một cam kết nói với React rằng "chạy cái này một lần rồi dừng lại."
Trường Hợp 2: Object Hoặc Array Dependency Bị Tạo Lại Mỗi Lần Render
JavaScript tạo ra một object mới trong bộ nhớ mỗi khi một hàm chạy. Hai object { page: 1 } và { page: 1 } không bằng nhau theo reference — React coi chúng là khác nhau, nên effect sẽ chạy lại.
// ❌ Lỗi — options nhận một reference mới mỗi lần render
const options = { page: 1, limit: 20 };
useEffect(() => {
fetchData(options);
setData(result);
}, [options]); // luôn "thay đổi"
Hai cách giải quyết sạch sẽ:
// ✅ Cách A: chuyển object ra ngoài component hoàn toàn
const OPTIONS = { page: 1, limit: 20 }; // tạo một lần, không bao giờ thay đổi
function MyComponent() {
useEffect(() => {
fetchData(OPTIONS);
}, []);
}
// ✅ Cách B: ổn định hóa bằng useMemo
const options = useMemo(() => ({ page: 1, limit: 20 }), []);
useEffect(() => {
fetchData(options);
}, [options]); // reference ổn định rồi
Trường Hợp 3: Function Dependency Bị Tạo Lại Mỗi Lần Render
Hàm cũng có vấn đề reference giống object. Định nghĩa một hàm trong body của component và nó sẽ là một hàm mới sau mỗi lần render.
// ❌ Lỗi — fetchUser là một hàm mới mỗi lần render
const fetchUser = () => {
fetch('/api/user').then(r => r.json()).then(setUser);
};
useEffect(() => {
fetchUser();
}, [fetchUser]); // luôn kích hoạt
Bọc nó trong useCallback để có được reference ổn định:
// ✅ Đã sửa
const fetchUser = useCallback(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []); // tạo một lần
useEffect(() => {
fetchUser();
}, [fetchUser]); // ổn định rồi
Trường Hợp 4: Cập Nhật State Không Có Điều Kiện Bảo Vệ
Dependency array không bảo vệ bạn khỏi chính bạn. Nếu bạn vô điều kiện set một state mà cũng được liệt kê là dependency, bạn đã tự tạo ra một vòng lặp hoàn hảo.
// ❌ Lỗi — items vừa là dependency vừa được set vô điều kiện
useEffect(() => {
setItems([...items, newItem]); // set items → re-render → effect chạy lại
}, [items]);
// ✅ Đã sửa — dùng dạng updater hàm và chỉ phụ thuộc vào newItem
useEffect(() => {
if (newItem) {
setItems(prev => [...prev, newItem]);
}
}, [newItem]);
Dạng hàm prev => [...prev, newItem] đọc state hiện tại từ bên trong — không cần liệt kê items là dependency nữa.
Trường Hợp 5: Vòng Lặp Fetch Dữ Liệu + Cập Nhật State
Cái này cả developer có kinh nghiệm cũng dính bẫy. Đưa state variable mà bạn sắp set vào dependency array chính là cái bẫy đó:
// ❌ Lỗi — user là dependency, nhưng setUser thay đổi user sau mỗi lần fetch
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId, user]); // user ở đây mới là vấn đề
// ✅ Đã sửa — chỉ fetch khi userId thay đổi
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
Kiểm Tra Sau Khi Sửa
- Mở Chrome DevTools → tab Console
- Hard-reload trang (Ctrl+Shift+R / Cmd+Shift+R)
- Xác nhận cảnh báo
Maximum update depth exceededđã biến mất - Mở React DevTools → tab Components — component đó phải ngừng nhấp nháy với các re-render liên tục
- Kiểm tra tab Network để xác nhận các API call không còn bắn ra trong vòng lặp (bạn nên thấy một request, không phải 50+)
Danh Sách Kiểm Tra Nhanh
useEffectcó dependency array không?- Có dependency nào là object hoặc array được định nghĩa trong body của component không?
- Có dependency nào là hàm được định nghĩa trong body của component không?
- State variable đang được set có đồng thời được liệt kê là dependency không?
- Việc cập nhật state có điều kiện hay luôn luôn thực thi?
Quy Tắc ESLint Giúp Phát Hiện Sớm
eslint-plugin-react-hooks được tích hợp sẵn với Create React App và Next.js. Nếu bạn dùng cấu hình tùy chỉnh, hãy cài thủ công:
npm install eslint-plugin-react-hooks --save-dev
// .eslintrc
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
Quy tắc exhaustive-deps sẽ đánh dấu các dependency bị thiếu hoặc sai ngay lúc viết code, trước khi bất cứ thứ gì đến được trình duyệt. Nó không hoàn hảo, nhưng bắt được các trường hợp rõ ràng — bao gồm hầu hết các mẫu đã đề cập ở trên.

