Hook 的规则只有两条:只在组件的顶层调用 Hook,绝不放在条件、循环、嵌套函数里,也不放在提前的 return 之后;并且只在函数组件或自定义 Hook 中调用。之所以有这两条规则,是因为 React 靠调用顺序来区分各个 Hook,所以每次渲染时这个顺序都必须相同。
React 把一个组件所有 Hook 的值存在挂在该组件上的一个列表里。每次渲染时它会依次遍历这个列表:第一次 Hook 调用拿到第一个槽位,第二次调用拿到第二个槽位,以此类推。这里没有名字,只有位置。下面的示例用几行普通 JavaScript 做了一个这种思路的玩具版本,并不是 React 自己的代码,所以你可以放心地把它弄坏。
第一次渲染填满了三个槽位:Ada、Countess、36。点击“Render without nickname”:条件里的调用被跳过了,于是 age 的调用拿到了槽位 1,读到的是 Countess。真正的 React 在某些情况下会发现不匹配并抛出错误,另一些情况下则会把错误的值交给错误的 Hook,后者更糟。把 toyUseState('Countess') 的调用移到 if 之上,两个按钮就都能显示正确的年龄了。
用代码看两条规则
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
}
}
只要一个 Hook 调用每次都会执行到,而且顺序不变,那它放在函数体里的哪个位置都可以,前提是它位于任何提前的 return 之前。
修复被条件调用的 Hook
linter 会把这种情况报告为 React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. 修复方法永远相同:每次都调用这个 Hook,把条件放到它里面。
// 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,由它内部的 if 决定要不要做事。多登录、退出几次,Console 会在每次登录时显示一条 subscribe,每次退出时显示一条 unsubscribe。
提前 return 之后的 Hook
提前的 return 也是一种条件。下面这段代码在 loading 第一次变成 false 时会报出“Rendered more hooks than during the previous render”,因为这次渲染执行到了一个上次渲染从未调用过的 Hook。当某次渲染调用的 Hook 比上一次少时,报错信息则是“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} />;
}
所有 Hook 都位于 return 之上,所以两个分支调用的是同样的 Hook。点击“Toggle loading”,展开列表,再切换两次 loading:列表依然记得自己是展开的,因为这个 state 从未被跳过。
循环中的 Hook
你不能在循环里为每一项调用一次 useState,因为项的数量会变化。应该给每一项单独写一个组件,在那里调用 Hook;每个实例都有自己的槽位。
“Invalid hook call”
这条报错信息以“Invalid hook call. Hooks can only be called inside of the body of a function component.”开头。常见原因有三个:
- Hook 运行时 React 并没有在渲染函数组件。 比如在事件处理函数里、Effect 回调里、类组件里或模块顶层。把它移到组件函数体或自定义 Hook 中,再从组件里调用这个自定义 Hook。
react和react-dom的版本不一致。 把两者安装成同一个版本。- 存在两份 React。 一个通过 link 引入的本地包,或者一个自带 React 的库,都会带来第二份 React,而一份 React 的 Hook 无法配合另一份的渲染器工作。运行
npm ls react,确保只出现一个版本。
npm ls react
这里的预览以生产构建的方式运行,不会打印这些信息:在事件处理函数里调用的 Hook 会以普通的 TypeError 失败,比如“Cannot read properties of null”,React 自己的错误则只显示为简短的“Minified React error”代码。要看到完整的报错文字,请在开发模式下运行你的应用。
让 linter 来把关
eslint-plugin-react-hooks 会在你输入时检查这两条规则,同时还会检查 Effect 的依赖数组。Vite 的 React 模板和 Next.js 的 ESLint 配置都已包含它。在 flat 格式的 ESLint 配置中,开启它的推荐预设:
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([reactHooks.configs.flat.recommended]);
两条核心规则是 react-hooks/rules-of-hooks(报错)和 react-hooks/exhaustive-deps(警告)。从版本 7 开始,这个预设还会开启来自 React Compiler 的规则,比如 react-hooks/purity 和 react-hooks/refs,它们会标记不纯的渲染以及在渲染期间读取 ref 的代码。如果想逐条挑选规则,就用 plugins: { 'react-hooks': reactHooks } 注册插件,再列出你需要的规则。
这个插件按名字识别 Hook:任何以 use 开头、后面紧跟大写字母的函数都会被当作 Hook,并且只有组件(首字母大写的名字)和其他 use 函数可以调用它。这就是自定义 Hook 必须使用这个前缀的原因。
例外:use
use 是 React 19 新增的,用来读取 Promise 或 context,而且和其他所有 Hook 不同,它可以在条件或循环里调用。不过它仍然必须在渲染期间、在组件或 Hook 中调用。
切换主题时,只有带主题的标题会变色。试着在 if 里改用 useContext 做同样的事,linter 就会标记出来。use Hook 页面讲解了如何用它读取 Promise。
常见问题
Hook 的规则有哪些?
一共两条。只在函数组件或自定义 Hook 的顶层调用 Hook,绝不放在条件、循环、嵌套函数里,也不放在提前 return 之后。并且只在函数组件或自定义 Hook 中调用 Hook,不要在普通函数或类组件中调用。
为什么不能有条件地调用 Hook?
React 并不是按名字认识你的 Hook。它把 Hook 的值存在一个列表里,再按调用发生的顺序依次交还。如果某个条件跳过了一次调用,后面每个 Hook 读到的都会是属于前一个 Hook 的值。
“Rendered more hooks than during the previous render”是什么意思?
这次渲染调用的 Hook 比上一次多,通常是因为某个 Hook 放在 if 里,或放在提前的 return 之后,而这次走了不同的分支。把每个 Hook 都移到条件之上,再把条件放进 Hook 内部。
如何修复“Invalid hook call”?
检查三件事:Hook 是在 React 渲染函数组件时运行的(不是在事件处理函数、Effect 回调、类组件或模块顶层中),react 和 react-dom 的版本一致,以及应用里只有一份 React(用 npm ls react 可以列出来)。
use 这个 Hook 可以有条件地调用吗?
可以。use(React 19)是唯一的例外:你可以在 if 块和循环里调用它。不过它仍然必须在组件或 Hook 中调用,不能在事件处理函数里调用。