Tình Huống Lỗi
Ứng dụng của bạn đang chạy bình thường. Bạn thêm một dòng if (!userId) return ở đầu component — code phòng thủ hợp lý — và bây giờ React ném ra lỗi:
Error: Rendered more hooks than during the previous render.
Toàn bộ ứng dụng crash. Component vừa render ngon lành hai phút trước giờ đã hỏng.
Đây chính xác là pattern kích hoạt lỗi này:
function UserProfile({ userId }) {
if (!userId) {
return <p>No user selected.</p>;
}
// ❌ Hook được gọi SAU một early return
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
Lần render đầu: userId có giá trị, React chạy cả hai hook. Lần render thứ hai: userId là undefined, early return được kích hoạt, và React thấy không có hook nào trong khi nó kỳ vọng có hai. Sự không khớp đó chính là nguyên nhân gây lỗi.
Tại Sao React Quan Tâm Đến Thứ Tự Hook
React theo dõi các hook theo vị trí, không phải theo tên. Mỗi lần render, nó duyệt qua một danh sách nội bộ có kích thước cố định — slot 0 là useState, slot 1 là useEffect, và cứ thế tiếp tục. Khi bạn đặt hook sau một conditional return, độ dài danh sách thay đổi giữa các lần render. React không còn biết state nào thuộc về hook nào nữa.
Đây chính là Rules of Hooks: gọi hook ở cấp cao nhất của component, không điều kiện, trong mọi lần render. Không có ngoại lệ.
Cách Sửa Nhanh: Chuyển Tất Cả Hook Lên Trước Mọi Return
Đưa mọi hook lên đầu hàm. Đặt các conditional return bên dưới chúng:
function UserProfile({ userId }) {
// ✅ Hook luôn chạy trước
const [user, setUser] = useState(null);
useEffect(() => {
if (!userId) return; // guard đặt bên trong effect
fetchUser(userId).then(setUser);
}, [userId]);
// Early return đặt SAU các hook
if (!userId) {
return <p>No user selected.</p>;
}
return <div>{user?.name}</div>;
}
Logic guard chuyển vào bên trong callback của useEffect. Bản thân hook vẫn chạy mỗi lần render — React hài lòng.
Ba Pattern Khác Vi Phạm Quy Tắc Này
Pattern 1: Hook bên trong câu lệnh if
// ❌ Sai
function Toggle({ isLoggedIn }) {
if (isLoggedIn) {
const [count, setCount] = useState(0); // bị bỏ qua khi chưa đăng nhập
}
return <div />;
}
// ✅ Đúng
function Toggle({ isLoggedIn }) {
const [count, setCount] = useState(0); // luôn chạy
return <div>{isLoggedIn ? count : null}</div>;
}
Pattern 2: Hook bên trong vòng lặp
Độ dài vòng lặp có thể thay đổi giữa các lần render — cùng vấn đề, khác hình thức.
// ❌ Sai
function ItemList({ items }) {
return items.map((item) => {
const [selected, setSelected] = useState(false);
return <Item key={item.id} selected={selected} />;
});
}
// ✅ Đúng — tách thành component con
function ItemList({ items }) {
return items.map((item) => <Item key={item.id} />);
}
function Item({ id }) {
const [selected, setSelected] = useState(false); // cấp cao nhất trong component riêng của nó
return <div onClick={() => setSelected(!selected)}>{id}</div>;
}
Pattern 3: Custom hook có điều kiện
// ❌ Sai
function DataFetcher({ shouldFetch, url }) {
if (shouldFetch) {
const data = useFetch(url); // đôi khi bị bỏ qua
}
}
// ✅ Đúng — truyền điều kiện vào trong hook
function DataFetcher({ shouldFetch, url }) {
const data = useFetch(shouldFetch ? url : null);
}
Thiết kế custom hook để chấp nhận giá trị null hoặc disabled thay vì bọc lời gọi trong một điều kiện. Hầu hết các thư viện hook được viết tốt (SWR, React Query) đều hỗ trợ sẵn pattern này.
Ngăn Chặn Từ Gốc: ESLint Plugin for Hooks
Cài plugin chính thức và bạn sẽ phát hiện lỗi này ngay trong editor trước khi ứng dụng chạy:
npm install eslint-plugin-react-hooks --save-dev
Cấu hình trong ESLint config của bạn:
// .eslintrc.js
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error', // cảnh báo hook có điều kiện
'react-hooks/exhaustive-deps': 'warn', // cảnh báo dependency bị thiếu
},
};
Create React App và template React của Vite đã bundle sẵn plugin này. Kiểm tra xem rules-of-hooks được đặt thành 'error', không phải 'warn' — warning rất dễ bị bỏ qua dưới áp lực deadline. Với 'error', linter sẽ không cho phép vi phạm qua mặt.
Xác Nhận Bản Sửa Đã Hoạt Động
- Lỗi đã biến mất — không còn crash trong browser console.
- ESLint sạch — chạy
npx eslint src/YourComponent.jsxvà xác nhận không còn vi phạmreact-hooks/rules-of-hooks. - Kiểm tra edge case — kích hoạt thủ công điều kiện gây ra early return (ví dụ truyền
userId={undefined}) và xác nhận UI fallback render không có lỗi. - React DevTools — mở component, bật tắt prop vài lần. State của hook phải cập nhật mượt mà không crash.
Checklist Khi Bạn Gặp Lỗi Này
- Rà soát mọi câu lệnh
returnxuất hiện trước một lời gọi hook. - Kiểm tra hook bên trong
if,switch, biểu thức ternary, hoặcArray.map(). - Tái cấu trúc custom hook có điều kiện để chấp nhận input disabled/null thay thế.
- Tách các item trong danh sách cần state riêng thành các component con độc lập.
- Bật
eslint-plugin-react-hooks— phát hiện lỗi lúc viết code, không phải lúc chạy.

