Rules of Hooks gồm hai quy tắc: chỉ gọi hook ở cấp cao nhất của component, không bao giờ trong điều kiện, vòng lặp, hàm lồng nhau hay sau một lệnh return sớm; và chỉ gọi hook từ function component hoặc từ custom hook. Hai quy tắc này tồn tại vì React phân biệt các hook theo thứ tự bạn gọi chúng, nên thứ tự đó phải giống nhau ở mọi lần render.
React lưu giá trị các hook của một component trong một danh sách gắn với component đó. Ở mỗi lần render, nó duyệt danh sách: lời gọi hook đầu tiên nhận ô đầu tiên, lời gọi thứ hai nhận ô thứ hai, cứ thế tiếp tục. Không có tên, chỉ có vị trí. Ví dụ dưới đây là một phiên bản mô phỏng ý tưởng đó trong vài dòng JavaScript thuần, không phải code của React, nên bạn có thể thoải mái làm hỏng nó.
Lần render đầu tiên lấp đầy ba ô: Ada, Countess, 36. Nhấn "Render without nickname": lời gọi trong điều kiện bị bỏ qua, nên lời gọi age chiếm ô 1 và đọc ra Countess. React thật phát hiện sự lệch này trong một số trường hợp và ném lỗi, còn trong những trường hợp khác thì đưa giá trị sai cho hook sai, điều này còn tệ hơn. Chuyển lời gọi toyUseState('Countess') lên trên if và cả hai nút đều cho ra đúng tuổi.
Hai quy tắc trong code
function Profile({ user }) {
// Good: always called, always in this order
const [tab, setTab] = useState('posts');
const theme = useContext(ThemeContext);
useEffect(() => {
document.title = user.name;
}, [user.name]);
// Bad: inside a condition, a loop or a nested function
if (user.isAdmin) {
const [log, setLog] = useState([]); // breaks the order
}
}
Một lời gọi hook luôn được chạy tới, theo cùng thứ tự, thì nằm ở đâu trong thân component cũng được, miễn là nó đứng trước mọi lệnh return sớm.
Sửa hook được gọi trong điều kiện
Linter báo lỗi này là React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. Cách sửa luôn giống nhau: gọi hook mọi lúc và đặt điều kiện vào bên trong nó.
// Broken: the effect exists on some renders and not others
function Notifications({ user }) {
if (user) {
useEffect(() => {
console.log('subscribe for', user.name);
}, [user]);
}
return null;
}
Effect được khai báo ở mọi lần render, và câu lệnh if bên trong nó quyết định có làm gì hay không. Đăng nhập rồi đăng xuất vài lần, Console sẽ hiện một dòng subscribe cho mỗi lần đăng nhập và một dòng unsubscribe cho mỗi lần đăng xuất.
Hook sau một lệnh return sớm
Một lệnh return sớm cũng là một điều kiện. Đoạn code này gây lỗi "Rendered more hooks than during the previous render" ở lần đầu loading chuyển thành false, vì lần render đó chạy tới một hook mà lần trước chưa từng gọi. Khi một lần render gọi ít hook hơn lần trước, thông báo sẽ là "Rendered fewer hooks than expected".
// Broken: useState is skipped while loading
function Comments({ loading, comments }) {
if (loading) return <p>Loading...</p>;
const [expanded, setExpanded] = useState(false);
return <List items={comments} expanded={expanded} />;
}
Mọi hook đều nằm trên lệnh return, nên cả hai nhánh gọi cùng các hook. Nhấn "Toggle loading", mở rộng danh sách, rồi bật tắt loading hai lần: danh sách vẫn nhớ là đang mở rộng, vì state chưa bao giờ bị bỏ qua.
Hook trong vòng lặp
Bạn không thể gọi useState một lần cho mỗi mục trong vòng lặp, vì số mục thay đổi. Hãy cho mỗi mục một component riêng và gọi hook trong đó; mỗi instance có các ô riêng của nó.
"Invalid hook call"
Thông báo bắt đầu bằng "Invalid hook call. Hooks can only be called inside of the body of a function component." Lỗi này thường có ba nguyên nhân:
- Hook chạy khi React không render một function component. Trong trình xử lý sự kiện, bên trong callback của effect, trong class component, hoặc ở cấp module. Hãy chuyển nó vào thân component hoặc một custom hook, rồi gọi custom hook đó từ component.
- Phiên bản
reactvàreact-domkhông khớp. Cài cùng một phiên bản cho cả hai. - Có hai bản React. Một package cục bộ được link hoặc một thư viện tự đóng gói React riêng sẽ mang theo bản thứ hai, và hook từ bản này không hoạt động với renderer của bản kia. Chạy
npm ls reactvà đảm bảo chỉ có một phiên bản xuất hiện.
npm ls react
Bản xem trước ở đây chạy như bản build production, vốn không in ra các thông báo này: một hook được gọi từ trình xử lý sự kiện sẽ lỗi với một TypeError đơn giản như "Cannot read properties of null", còn lỗi của chính React chỉ hiện dưới dạng mã ngắn "Minified React error". Chạy ứng dụng ở chế độ development để đọc thông báo đầy đủ.
Để linter kiểm tra giúp bạn
eslint-plugin-react-hooks kiểm tra cả hai quy tắc ngay khi bạn gõ, và kiểm tra cả mảng dependency của effect. Template React của Vite và cấu hình ESLint của Next.js đều có sẵn plugin này. Trong cấu hình ESLint dạng flat, hãy bật preset recommended của nó:
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([reactHooks.configs.flat.recommended]);
Hai quy tắc cốt lõi là react-hooks/rules-of-hooks (báo lỗi) và react-hooks/exhaustive-deps (cảnh báo). Từ phiên bản 7, preset này còn bật các quy tắc đến từ React Compiler, như react-hooks/purity và react-hooks/refs, giúp phát hiện render không thuần và việc đọc ref trong lúc render. Để chọn từng quy tắc, hãy đăng ký plugin dưới dạng plugins: { 'react-hooks': reactHooks } và liệt kê những quy tắc bạn muốn.
Plugin nhận ra hook theo tên: mọi hàm có tên bắt đầu bằng use theo sau là một chữ in hoa đều được xem là hook, và chỉ component (tên viết hoa chữ đầu) cùng các hàm use khác mới được gọi nó. Đó là lý do custom hook phải dùng tiền tố này.
Ngoại lệ: use
use, được thêm vào từ React 19, đọc một promise hoặc một context, và khác với mọi hook khác, nó có thể được gọi trong điều kiện hoặc vòng lặp. Nó vẫn phải được gọi trong lúc render, bên trong component hoặc hook.
Chỉ tiêu đề có theme đổi màu khi bạn chuyển theme. Thử làm tương tự với useContext bên trong if và linter sẽ báo lỗi. Trang hook use hướng dẫn cách đọc promise bằng nó.
Câu hỏi thường gặp
Rules of Hooks gồm những quy tắc nào?
Có hai quy tắc. Chỉ gọi hook ở cấp cao nhất của function component hoặc custom hook, không bao giờ trong điều kiện, vòng lặp, hàm lồng nhau hay sau một lệnh return sớm. Và chỉ gọi hook từ function component hoặc custom hook, không gọi từ hàm thông thường hay class component.
Vì sao không được gọi hook trong điều kiện?
React không biết hook của bạn theo tên. Nó lưu giá trị của chúng trong một danh sách và trả lại theo thứ tự các lời gọi diễn ra. Nếu một điều kiện bỏ qua một lời gọi, mọi hook phía sau sẽ đọc giá trị vốn thuộc về hook đứng trước nó.
Lỗi "Rendered more hooks than during the previous render" nghĩa là gì?
Một lần render đã gọi nhiều hook hơn lần render trước, thường do một hook nằm trong if hoặc sau một lệnh return sớm mà lần này chạy khác đi. Hãy đưa mọi hook lên trên điều kiện và đặt điều kiện vào bên trong hook.
Sửa lỗi "Invalid hook call" thế nào?
Kiểm tra ba điều: hook chạy trong lúc React render một function component (không phải trong trình xử lý sự kiện, callback của effect, class component hay ở cấp module), react và react-dom cùng phiên bản, và ứng dụng chỉ có một bản React (npm ls react liệt kê chúng).
Hook use có được gọi trong điều kiện không?
Có. use (React 19) là ngoại lệ duy nhất: bạn có thể gọi nó trong khối if và vòng lặp. Nó vẫn phải được gọi bên trong component hoặc hook, không phải trong trình xử lý sự kiện.