
一句话:React 用链表按「调用顺序」存储每个组件的 Hooks 状态,因此 Hooks 必须在每次渲染中以完全相同的顺序、相同次数被调用——这就是「不能在条件/循环里写 Hook」的根本原因。
一、两条规则
① 只能在【顶层】调用
❌ 不能在条件、循环、嵌套函数、try/catch 里调用
② 只能在【函数组件】或【自定义 Hook】中调用
❌ 不能在普通 JS 函数、类组件、事件处理函数里调用
// ❌ 违反规则一:写在条件里
function Comp({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // ⚠️ 顺序会变
}
const [theme, setTheme] = useState('light');
// ...
}
// ❌ 违反规则一:写在循环里
function Comp2({ list }) {
const refs = list.map(() => useRef(null)); // ⚠️ 数量会变
}
// ❌ 违反规则一:写在 try/catch 里
function Comp3() {
try {
const [x, setX] = useState(0); // ⚠️
} catch {}
}
// ❌ 违反规则一:写在嵌套函数里
function Comp4() {
const handler = () => {
const [x, setX] = useState(0); // ⚠️
};
}
// ✅ 正确:全部写在组件函数体的顶层
function Good({ isLoggedIn }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
// 条件逻辑放在 Hook 调用【之后】
const displayName = isLoggedIn ? user?.name : '访客';
return {displayName} / {theme};
}
// ❌ 违反规则二:在普通函数里调用
function formatData(data) {
const [x, setX] = useState(0); // ⚠️ 报错:Invalid hook call
}
// ✅ 如果是「复用逻辑」,写成自定义 Hook(use 开头)
function useFormattedData(data) {
const [x, setX] = useState(0);
return x;
}
二、为什么不能写在条件里:链表结构
React 为每个组件维护一个 fiber 节点,节点上有 memoizedState 字段:
fiber.memoizedState → hook1 → hook2 → hook3 → null
↓ ↓ ↓
{state} {state} {state} (单向链表,按调用顺序相连)
每次渲染时,React 用一个「游标」从链表头部开始,
每调用一个 Hook 就把游标往后移一位,读取/写入对应的 hook 对象。
// 用伪代码模拟 React 内部的处理
let currentHookNode = null; // 当前正在处理的 hook 节点
let isFirstRender = true;
function renderWithHooks(Component, props) {
currentHookNode = ComponentFiber.memoizedState; // ⭐ 从头开始
const result = Component(props); // 执行组件函数(Hooks 被依次调用)
ComponentFiber.memoizedState = currentHookNode; // 保存(首次渲染时即建立链表)
return result;
}
function useState(initial) {
let hook;
if (isFirstRender) {
// 首次渲染:创建节点并接到链表尾部
hook = { memoizedState: typeof initial === 'function' ? initial() : initial, queue: [] };
if (currentHookNode === null) {
CurrentFiber.memoizedState = hook; // 链表头
} else {
currentHookNode.next = hook; // 接到尾部
}
currentHookNode = hook;
} else {
// 更新渲染:按【顺序】读取已有节点
hook = currentHookNode;
currentHookNode = hook.next; // ⚠️ 游标后移
// 消费 update 队列
if (hook.queue.length) {
hook.memoizedState = hook.queue[hook.queue.length - 1].newValue;
}
}
const setState = (action) => {
hook.queue.push({ newValue: typeof action === 'function' ? action(hook.memoizedState) : action });
scheduleRerender(); // 触发重渲染
};
return [hook.memoizedState, setState];
}
如果 Hook 的调用顺序变了会怎样?
首次渲染(isLoggedIn = false): 更新渲染(isLoggedIn = true):
useState(user) → 节点 0 if (isLoggedIn) useState(user) → 节点 0 ❌
useState(theme) → 节点 1 useState(theme) → 节点 1 ❌
↑ 顺序错位:theme 读到了 user 的节点
后果:
① 状态错乱(theme 拿到了 user 的值)
② 报错:Rendered more hooks than during the previous render
③ 未知副作用(可能崩溃)
结论:Hook 的身份由「调用顺序」决定,而不是由变量名或位置决定。所以顺序必须稳定。
三、Hook 对象的内部结构
// 每个 Hook 节点大致长这样(不同类型字段略有差异)
{
memoizedState: any, // 当前的状态值(useEffect 里存的是 effect 对象)
baseState: any, // 基础状态(用于更新队列的基线)
baseQueue: any,
queue: UpdateQueue, // 更新队列(setState 产生的更新会进这里)
next: Hook | null, // 指向下一个 Hook
}
// 不同 Hook 存的东西不同:
// useState → memoizedState: 状态值;queue: 更新队列
// useRef → memoizedState: { current: ... } (注意:useRef 的节点只创建一次)
// useEffect → memoizedState: effect 对象(含 create / destroy / deps)
// useMemo → memoizedState: [value, deps]
// useCallback → memoizedState: [callback, deps]
// useReducer → memoizedState: 状态值;queue: 参数化的 dispatch 队列
// 观察 Hook 链表(在 React DevTools 里可以看到组件的 Hooks 列表)
function Demo() {
const [a, setA] = useState(1); // Hook 0
const [b, setB] = useState('x'); // Hook 1
const ref = useRef(null); // Hook 2
useEffect(() => {}, []); // Hook 3
const memo = useMemo(() => a + 1, [a]); // Hook 4
// ...
}
// DevTools 中显示为:State(1) → State('x') → Ref → Effect → Memo → ...
四、为什么「Hook 只能在组件里调用」
React 需要知道「当前正在渲染哪个组件」,才能把 Hook 的状态挂到对应的 fiber 上。
这个信息保存在一个「当前正在渲染的 fiber」变量里(ReactCurrentDispatcher / currentFiber)。
这个变量只在 React 渲染组件的过程中被设置。
在普通函数里调用 Hook 时,这个变量是 undefined 或指向别的组件
→ React 抛错:"Invalid hook call. Hooks can only be called inside of the body of a function component."
// 报错信息的三种常见成因(React 会提示这三种可能)
// 1. React 与 ReactDOM 版本不匹配
// 2. 违反 Hook 规则(在非组件里调用)
// 3. 同一个应用里有【多份 React 副本】(如依赖重复安装)
// ③ 的排查:npm ls react 检查是否有多个版本
五、ESLint 规则
// .eslintrc / eslint.config.js
{
"extends": ["plugin:react-hooks/recommended"]
// 包含两条规则:
// react-hooks/rules-of-hooks → 检查 Hook 的调用位置(error)
// react-hooks/exhaustive-deps → 检查依赖数组是否完整(warn)
}
// rules-of-hooks 能捕获的问题
function Comp({ flag }) {
if (flag) {
useEffect(() => {}); // ✅ ESLint 报错:React Hook "useEffect" is called conditionally
}
}
// exhaustive-deps 的提示
function Comp2({ id }) {
useEffect(() => {
fetchData(id); // ✅ ESLint 警告:React Hook useEffect has a missing dependency: 'id'
}, []); // ⚠️ 应该写成 [id]
}
六、违反规则的常见场景与正确写法
// ① 条件 Hook → 提到顶层 + 在逻辑里判断
// ❌
if (isEnabled) {
const [value, setValue] = useState(0);
}
// ✅
const [value, setValue] = useState(0);
if (!isEnabled) return null; // 条件影响的是「渲染结果」,不是 Hook 调用
// ② 循环 Hook → 用「一个 Hook 管理数组」
// ❌
items.map(() => useRef(null));
// ✅
const refs = useRef
七、常见坑
// ① 在条件里调用 Hook
if (cond) useSomething();
// ② 在循环里调用 Hook
items.forEach(() => useSomething());
// ③ 在 Hook 之前提前 return
if (!data) return null;
const [x] = useState(0); // ❌
// ④ 在事件处理函数/普通函数里调用
const handleClick = () => { const [x] = useState(0); }; // ❌
// ⑤ 在类组件里用 Hook
class A extends React.Component {
render() { const [x] = useState(0); } // ❌
}
// ⑥ 依赖重复安装的 React
// 报 "Invalid hook call" 但代码没问题 → 检查 npm ls react
// 常见于:组件库把 react 放进了 dependencies(应该放 peerDependencies)
// ⑦ 用条件表达式「看起来」没变但顺序变了
{cond ? useA() : useB()} // ❌ 两者都是 Hook,但只调一个
// ⑧ 在 useState 的初始化函数里调用别的 Hook
useState(() => { useSomething(); return 0; }); // ❌ 回调里不能调 Hook
// ⑨ 忘记 ESLint 规则(项目里没配 react-hooks)
// → 这类 bug 很隐蔽,必须开 lint
// ⑩ 自定义 Hook 命名不以 use 开头
function fetchUser() { const [x] = useState(0); } // ⚠️ lint 检查不到,且语义不清
function useFetchUser() { const [x] = useState(0); } // ✅
面试延伸
- 「为什么 Hooks 不能写在条件或循环里?」
因为 React 用链表存储每个组件的 Hooks 状态,并且靠「调用顺序」来匹配(第 1 个 Hook 对应链表的第 1 个节点)。如果某次渲染中顺序或数量变了(某个 Hook 被跳过或多调用),React 就会把状态写到错误的节点上——表现为状态错乱、Rendered more hooks than during the previous render 报错等。所以规则是「每次渲染必须以完全相同的顺序、相同的次数调用所有 Hook」。
- 「React 怎么知道当前是哪个 Hook 的?」
靠一个「游标」:每次渲染开始时,游标指向该组件 Hook 链表的头部;每调用一个 Hook,就把游标往后移一位,读/写对应节点;渲染结束后链表会被更新(首次渲染时建立链表)。此外 React 内部维护一个「当前正在渲染的 fiber」变量,用来确定「这些 Hook 属于哪个组件」——这也是「普通函数里调用 Hook 会报 Invalid hook call」的原因。
- 「Hook 对象内部存了什么?」
每个 Hook 是链表上的一个节点,包含 memoizedState(当前状态)、queue(更新队列)、next(指向下一个 Hook)。不同 Hook 存的 memoizedState 不同:useState/useReducer 存状态值;useRef 存 { current }(且只创建一次);useEffect 存 effect 对象(含 create / destroy / deps);useMemo / useCallback 存 [值, 依赖]。
- 「
Invalid hook call报错有哪些可能原因?」
React 的错误提示里列了三种:① 违反了 Hook 规则(在条件/循环/普通函数里调用);② React 与 ReactDOM 版本不匹配(升级时只升了一个);③ 应用里存在多份 React 副本(典型原因是某个组件库把 react 写进了 dependencies 而不是 peerDependencies,导致装了两份)。排查方式是 npm ls react 看是否有重复版本。
- 「为什么 Hook 必须写在组件函数体的顶层?」
因为 Hook 的执行需要「当前正在渲染的组件上下文」——React 需要知道把状态挂到哪个 fiber 上,以及当前链表游标在哪。写在嵌套函数或回调里时,调用时机不确定(可能不在渲染过程中),React 无法确定上下文,于是抛错。写在顶层能保证「每次渲染都按固定顺序执行」。
- 「
if (!data) return null写在 Hook 之前有什么问题?」
会导致「Hook 的调用数量不稳定」:当 data 存在时,后续的 Hook 会被调用;当 data 为空时提前 return,后续 Hook 被跳过 → 两次渲染的 Hook 数量不同 → React 报错(Rendered fewer hooks than expected)或状态错乱。正确写法是把所有 Hook 都写在最前面,条件 return 放在所有 Hook 之后。
- 「自定义 Hook 必须用
use开头吗?」
必须。原因有两个:① ESLint 的 rules-of-hooks 规则靠命名来识别——只有 use 开头的函数才会被当作「可能包含 Hook 的地方」来检查,不这么命名会让 lint 失效,隐藏 bug;② 语义约定——use 开头明确告诉调用者「这是一个包含状态/副作用的 Hook,必须遵守 Hook 规则」。所以即使只是复用逻辑,只要用到 Hook 就必须 use 开头。
- 「Hook 的调用顺序怎么保证在组件复用时也不错乱?」
关键在于「自定义 Hook 内部的 Hook 调用顺序也必须是稳定的」。自定义 Hook 只是把一组 Hook 调用封装起来,它对宿主组件来说就是「按顺序插入的一段 Hook」。所以:① 自定义 Hook 内部同样不能有条件/循环;② 调用多个自定义 Hook 的顺序也必须稳定(不能有条件地调用某个自定义 Hook)。这也是为什么「抽 Hook」不能解决「条件调用」的问题。
一句话速记
React 用链表 + 调用顺序来匹配 Hook 的状态(
fiber.memoizedState → hook1 → hook2 → …),所以 Hooks 必须在每次渲染中以相同顺序、相同次数出现在组件函数体的顶层;不能写在条件、循环、嵌套函数、try/catch里,也不能在所有 Hook 之前提前return;「动态数量」的场景用「一个 Hook 管数组/Map」而不是循环调用;必须开eslint-plugin-react-hooks;报Invalid hook call还要检查「是否装了多份 React」(npm ls react)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!