Sửa lỗi "Too many re-renders" trong React Component

intermediate⚛️ React2026-03-19| React 16.8+, mọi hệ điều hành, mọi trình duyệt — xảy ra trên cả môi trường development và production

Error Message

Too many re-renders. React limits the number of renders to prevent an infinite loop.
#react#re-render#vòng-lặp-vô-hạn#state

Lỗi gặp phải

Too many re-renders. React limits the number of renders to prevent an infinite loop.

React dừng hoạt động sau khi đạt đến giới hạn render — khoảng 25 chu kỳ trong môi trường development. Ứng dụng bị đóng băng, console tràn ngập cùng một thông báo lỗi, và đôi khi tab trình duyệt bị crash hoàn toàn. Hầu hết trường hợp đều do một component đang tự lên lịch re-render sau mỗi lần vẽ giao diện.

Nguyên nhân gốc rễ

Ba mẫu code sau gây ra hầu hết các trường hợp:

  • Gọi state setter trực tiếp trong phần thân render — không phải bên trong handler hoặc effect
  • Truyền một hàm đã được gọi thực thi vào event prop thay vì truyền tham chiếu hàm
  • Một useEffect có dependency thay đổi sau mỗi lần render, tạo ra vòng lặp

Cách sửa 1: State setter bị gọi trong quá trình render

Đây là nguyên nhân phổ biến nhất. Setter kích hoạt, gây ra re-render, rồi setter lại kích hoạt tiếp. React sẽ dừng lại ở khoảng ~25 vòng lặp trước khi trình duyệt bị treo.

// ❌ Sai — setCount chạy sau mỗi lần render
function Counter() {
  const [count, setCount] = useState(0);
  setCount(count + 1); // gọi trực tiếp trong phần thân render
  return <div>{count}</div>;
}

// ✅ Sửa — chuyển vào useEffect hoặc event handler
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // chạy sau khi mount, không phải trong quá trình render
  }, []); // deps rỗng = chỉ chạy một lần

  return <div>{count}</div>;
}

Cách sửa 2: Gọi hàm thay vì truyền hàm

Chỉ một cặp dấu ngoặc thừa là gây ra vấn đề này. onClick={handleClick()} gọi hàm ngay lập tức trong quá trình render. onClick={handleClick} truyền nó dưới dạng tham chiếu để React gọi sau, khi người dùng click.

// ❌ Sai — handleClick() thực thi trong quá trình render, không phải khi click
function MyButton() {
  const [open, setOpen] = useState(false);

  function handleClick() {
    setOpen(true);
  }

  return <button onClick={handleClick()}>Open</button>;
}

// ✅ Sửa — truyền tham chiếu, không có dấu ngoặc
return <button onClick={handleClick}>Open</button>;

Cần truyền tham số? Hãy bọc nó trong arrow function:

// ✅ Arrow function — an toàn, không gọi thực thi khi render
return <button onClick={() => handleClick(someId)}>Open</button>;

Cách sửa 3: Vòng lặp dependency trong useEffect

Một effect thay đổi một trong các dependency của chính nó sẽ chạy mãi mãi. State thay đổi → effect chạy lại → state lại thay đổi. Phá vỡ vòng lặp này bằng cách loại bỏ biến state khỏi mảng dependency.

// ❌ Sai — items thay đổi → effect chạy → items thay đổi → lặp vô hạn
useEffect(() => {
  setItems([...items, newItem]);
}, [items]);

// ✅ Sửa — functional updater đọc state mới nhất mà không cần đưa vào deps
useEffect(() => {
  setItems(prev => [...prev, newItem]);
}, [newItem]); // chỉ chạy lại khi newItem thay đổi

Cách sửa 4: Object hoặc array inline làm dependency

Trường hợp này khá tinh vi. JavaScript tạo ra một tham chiếu object hoàn toàn mới sau mỗi lần render, ngay cả khi các giá trị bên trong giống hệt nhau. React dùng so sánh tham chiếu để kiểm tra dependency — vì vậy { id: 1 } viết inline luôn bị coi là "đã thay đổi".

// ❌ Sai — { id: 1 } là một tham chiếu mới sau mỗi lần render
useEffect(() => {
  fetchData({ id: 1 });
}, [{ id: 1 }]);

// ✅ Sửa — ổn định bằng useMemo để tham chiếu không thay đổi giữa các lần render
const params = useMemo(() => ({ id: 1 }), []);

useEffect(() => {
  fetchData(params);
}, [params]);

Cách sửa 5: setState có điều kiện không bao giờ dừng

Đặt state bên trong khối if trông có vẻ vô hại — nhưng khi điều kiện luôn đúng, nó sẽ kích hoạt sau mỗi lần render.

// ❌ Sai — điều kiện luôn đúng khi data tồn tại
function Form({ data }) {
  const [value, setValue] = useState('');

  if (data) {
    setValue(data.name); // chạy mỗi lần render miễn là data có giá trị
  }

  return <input value={value} />;
}

// ✅ Sửa — chuyển việc đồng bộ vào useEffect với data làm dependency
function Form({ data }) {
  const [value, setValue] = useState('');

  useEffect(() => {
    if (data) {
      setValue(data.name);
    }
  }, [data]); // chỉ chạy khi data thực sự thay đổi

  return <input value={value} />;
}

Cách xác định component nào đang bị lặp

Stack trace của React thường trỏ vào code nội bộ fiber, không phải file của bạn. Dùng các bước sau để xác định chính xác thủ phạm:

  • Mở React DevTools → tab Profiler → nhấn Record → component đang lặp sẽ hiển thị 50–200 lần render trong chưa đầy một giây
  • Thêm console.count('MyComponent') vào đầu mỗi component nghi ngờ — cái nào in ra 100+ lần là thủ phạm
  • Comment từng useEffect ra một cho đến khi vòng lặp dừng — đó chính là hook gây ra vấn đề
  • Kiểm tra tất cả event prop (onClick, onChange, onSubmit) xem có dấu () thừa sau tên handler không

Xác nhận đã sửa xong

  • Lỗi không còn xuất hiện trong console
  • React DevTools Profiler cho thấy component chỉ render khi có tương tác của người dùng hoặc khi component cha re-render — không tự render nữa
  • console.count ngừng tăng sau lần mount đầu tiên
  • Tab Network xác nhận các API call chỉ gửi một lần mỗi hành động, không phát liên tục

Danh sách kiểm tra phòng tránh

  • State setter phải nằm trong event handler hoặc effect — không bao giờ trong phần thân render
  • Event prop nhận tham chiếu: onClick={fn}, không phải lời gọi hàm: onClick={fn()}
  • Bên trong useEffect, ưu tiên dùng functional updater (prev => ...) và bỏ biến state ra khỏi mảng dependency
  • Bọc các object và array dùng làm effect dependency bằng useMemo hoặc useCallback để ổn định tham chiếu
  • Thêm eslint-plugin-react-hooks vào dự án — rule exhaustive-deps sẽ phát hiện hầu hết các vấn đề này trước khi chúng xuất hiện trên trình duyệt

Related Error Notes