React Hooks 为什么不能写在条件语句或循环里?
简化版
Hooks 依赖调用顺序来关联每个 hook 的状态。如果把 hook 写在条件或循环里,不同 render 的调用顺序可能变化,React 就无法把状态和正确的 hook 对上。因此除 React 19 的 use(resource) 特例外,常规 Hooks 必须写在函数组件或自定义 Hook 的顶层。
详细版
错误写法:
if (visible) {
const [count, setCount] = useState(0);
}
如果上一轮 visible 是 true,下一轮是 false,hook 调用数量变了,后面的 hook 顺序都会错位。
规则:
- 只能在 React 函数组件或自定义 Hook 中调用 Hook。
- 常规 Hooks 只能在顶层调用,不能放进条件、循环、嵌套函数;React 19 的
use特例在完整版中单独说明。
条件逻辑应该放到 hook 内部:
useEffect(() => {
if (!visible) return;
// ...
}, [visible]);
完整版教学
一、Hooks 状态靠顺序存储
函数组件没有类实例字段,React 需要在 Fiber 节点上保存 hook 链表。每次组件执行时,React 按调用顺序依次取出对应 hook 的状态。
第一个 useState 对应第一份状态,第二个 useEffect 对应第二份 effect。顺序稳定,React 才能对上号。
二、条件调用会破坏顺序
如果某次 render 多调用一个 hook,下一次少调用一个,后续 hook 的位置都会偏移。React 读到的状态就会错位,产生难以解释的 bug。
这就是为什么规则看起来严格,但本质是为了让函数组件的状态模型可预测。
三、自定义 Hook 也遵守同样规则
自定义 Hook 只是把 hook 逻辑封装成函数,内部仍然必须稳定调用 hook。调用自定义 Hook 本身也要在组件顶层。
function useUser(id) {
return useQuery(['user', id], fetchUser);
}
它能复用逻辑,但不能绕过 hook 规则。
四、面试追问与工程落地
常见追问是“那条件副作用怎么写”。答案是 hook 仍然顶层调用,把条件放进回调内部,或者让依赖控制执行。
还会问“eslint 为什么能检查”。eslint-plugin-react-hooks 通过静态分析识别 hook 调用位置和依赖数组,能提前发现大部分违反规则的问题。
工程中遇到“某个条件下才需要状态”,通常应该把条件拆成子组件。子组件是否渲染由父组件控制,子组件内部 hook 顺序仍然稳定。
五、用两轮调用演示状态错位
可以把某个 Fiber 上的 Hook 状态想成按顺序连接的槽位,而变量名并不参与匹配。第一轮调用 useState(A) → useEffect(B) → useState(C),第二轮若条件跳过第一个 Hook,React 接下来会把原本属于 A 的槽位交给 B,后续全部错位。
第 1 轮:槽位 1 = state A | 槽位 2 = effect B | 槽位 3 = state C
第 2 轮:跳过 A | 第一个调用想读取 B,却先遇到槽位 1
结果:调用序列与保存序列无法一一对应
假设循环长度从 3 变成 2,循环内的 Hook 数量也从 3 次变成 2 次,循环后所有 Hook 的序号都会提前 1 位。规则限制的是调用位置,而不是条件表达式本身;因此可以始终调用 Hook,再在 effect 回调或渲染结果里判断条件。
六、区分普通 Hooks 与 React 19 的 use 特例
useState、useEffect、useMemo、自定义 Hook 等仍必须在组件或自定义 Hook 顶层调用。React 19 的 use(resource) 是明确特例,可以出现在条件和循环中,用于读取 Promise 或 Context;但仍只能在组件或 Hook 的渲染过程中调用,也不能用 try/catch 包住对 use 的调用。
if (shouldRead) {
const theme = use(ThemeContext); // React 19 的 use 特例
}
这个例外不代表传统 Hooks 改成了按变量名匹配,更不能把 useState 搬进同样的 if。面试时先回答稳定调用顺序,再主动补充 use 特例,能同时覆盖经典原理和当前版本边界。
| 调用 | 条件/循环中调用 | 仍需满足的边界 |
|---|---|---|
useState/useEffect 等 Hooks | 不允许 | 组件或自定义 Hook 顶层 |
| 自定义 Hook | 不允许 | 调用序列必须稳定 |
React 19 use(resource) | 允许 | 只能在组件/Hook 渲染中,不能置于 try/catch |
心法:普通 Hooks 看“每轮调用序列是否一致”;React 19 的
use是资源读取原语,规则单独记,不能类推到 useState/useEffect。
七、常见误区与追问
- 误区:Hook 变量名相同就能对应到原状态。 React 依赖组件 Fiber 上的调用顺序,不依赖局部变量名。
- 误区:只要条件永远不变,就可以把 useState 写进 if。 代码契约仍不可靠,静态规则也会报错;应把条件放进 Hook 内或拆组件。
- 误区:自定义 Hook 是普通函数,所以可以任意位置调用。 它内部仍调用 Hooks,调用它的位置也必须满足 Rules of Hooks。
- 追问:React 19 的 use 能写在条件里吗? 可以,这是官方特例,但
useState、useEffect等规则没有因此放宽。 - 追问:条件副作用应该怎样实现? 顶层声明 effect,在 setup 内判断条件,并保证依赖完整与 cleanup 对称。
- 追问:为什么拆子组件能解决条件 Hook? 父组件条件决定子树是否存在,子组件每次实际渲染时内部 Hook 序列仍然固定。
- 追问:eslint 能证明依赖一定正确吗? 它能静态发现常见违规,动态生成或刻意绕过时仍需开发者保证组件纯度和依赖语义。
八、加强记忆
Hooks 靠“调用顺序”认状态,不靠变量名认状态。顺序不能变,所以 hook 必须在顶层;条件逻辑放进 hook 内部或拆组件。
React 19 的 use 单独作为资源读取特例记忆,绝不能反推普通 Hooks 也能条件调用。