Fix useEffect Infinite Loop: Warning Maximum Update Depth Exceeded trong React

intermediate⚛️ React2026-03-20| React 16.8+, mọi hệ điều hành (Windows / macOS / Linux), Node.js 14+

Error Message

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect.
#react#useEffect#infinite-loop#dependency-array

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
  • useEffect chạy và gọi setState
  • State thay đổi kích hoạt re-render
  • Re-render khiến useEffect chạ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 }{ 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

  • useEffect có 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.

Related Error Notes