Warning: React has detected a change in the order of Hooks called by com.
最近在使用了 react hooks, 进入条件渲染的时候, 出现了错误.
警告:React已检测到com调用的Hook的顺序发生了变化。
以下是代码的简单形式, 即 httpData 是从 ajax 请求得来的, 当 httpData 有数据时, 才动态渲染 tabList 中的组件.
这在 class 形式的 react 组件中是很普通和常用的操作, 即使用条件运行符进行条件渲染, 可是这里却收到错误.
const tabList = {
Headers: Headers, // Headers 是一个组件, 这个组件里也使用了 hooks, 例如 useState
// Headers: (props = {}) => {
// return (
// <div>
// 数据: {JSON.stringify(props, null, 2)}
// </div>
// )
// },
Preview: () => `Preview`,
// // ...
}
return (
<div>
{
Object.keys(tabList).map(key => (
<div key={key}>
{state.httpData ? tabList[key]({httpData: state.httpData}) : `(暂无)`}
</div>
))
}
</div>
)
分析
首先判断可能是使用 react hooks 的一些规则与 class 的形式不一致导致的问题.
然后使用错误信息直接 google baidu 了一圈, 大概信息是不要使用条件渲染带 hooks 的组件.
并且别人示例上的解决方案好像都不适合自己.
然后又看了官网文档 https://fb.me/rules-of-hooks .
发现 hooks 有以下信息:
Hooks 是 JavaScript 函数,但在使用它们时需要遵循两个规则。 我们提供了一个 linter 插件 来自动执行这些规则:
只在顶层调用Hook
**不要在循环,条件或嵌套函数中调用 Hook **。 相反,总是在 React 函数的顶层使用 Hooks。 通过遵循此规则,您可以确保每次组件渲染时都以相同的顺序调用 Hook 。 这就是允许 React 在多个 useState 和 useEffect 调用之间能正确保留 Hook 状态的原因。 (如果你很好奇,我们将在 下面 深入解释。)
只在 React Functions 调用 Hooks
不要在常规 JavaScript 函数中调用 Hook 。 相反,你可以:
- 在 React 函数式组件中调用 Hooks 。
- 从自定义 Hooks 调用 Hooks (我们将在下一页中学习 自定义 Hooks )。
通过遵循此规则,您可以确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。
总的说来, 看起来是:
- 不要把 jsx 风格与 js 风格的函数混用. (虽然 jsx 里兼容 js, 这点能理解, 比如你写了个工具函数, 想放在其他项目里用, 但其他项目由于不是 jsx, 并不认识
return <div></div>
之类的语法) - 不要在循环,条件或嵌套函数中调用 Hook. (这个我就不太理解了, 只能在顶层函数里用 hook, 那还怎么职责分离? 我子组件也是函数式的, 子组件里不能用 hook? 越想越不对劲, hooks 没这么鸡肋吧?)
- 如果你很好奇,我们将在 下面 深入解释。(那就往下看)
// ------------
// 第一次渲染
// ------------
useState('Mary') // 1. 用'Mary'初始化名称状态变量
useEffect(persistForm) // 2. 添加一个 effect 用于持久化form
useState('Poppins') // 3. 使用 'Poppins' 初始化 surname 状态变量
useEffect(updateTitle) // 4. 添加一个 effect 用于更新 title
// 🔴 我们在条件语句中使用Hook,打破了第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
// -------------
// 第二次渲染
// -------------
useState('Mary') // 1. 读取 name 状态变量(忽略参数)
// useEffect(persistForm) // 🔴 这个Hook被跳过了
useState('Poppins') // 🔴 2 (但是之前是 3). 读取 surname 状态变量失败
useEffect(updateTitle) // 🔴 3 (但是之前是 4). 替换 effect 失败
// ...
看起来好像有点道理, 由于第二次 hook 与第一次不一样, react 不知道第二次应该渲染成什么(我就好奇: 不能渲染为空吗? 求解答).
然后文档里给出的方案是:
useEffect(function persistForm() {
// 👍 我们不再违反第一条规则了
if (name !== '') {
localStorage.setItem('formData', name);
}
});
这? 感觉像是在耍无赖! 我如何变换为我所需要的条件渲染呢?
分析2
看来只能好好理解上面的规则, 以及分析和调试自己的代码了.
如果把代码改成这样, 不报错.
// Headers: Headers, // Headers 是一个组件, 这个组件里也使用了 hooks, 例如 useState
Headers: (props = {}) => {
return (
<div>
数据: {JSON.stringify(props, null, 2)}
</div>
)
},
改成这样 (在 Headers 组件不使用任何 hooks, 如 useState), 也不报错:
// const [state, setState] = useState(initState);
return `test`
原因慢慢暴露出来, 好像就是在子组件中使用 hook 的原因. 首先我是相信, hook 不可能真的不能在子组件中使用的.
那么可能就是另外一个原因: 不能 jsx 与 js
风格混用.
尝试修改为这样:
{
state.httpData ?
(() => {
const ComName = tabList[key]
return <ComName {...{httpData: state.httpData}} />
})()
: `(暂无)`
}
// {httpData ? tabList[key]({httpData: httpData}) : `(暂无)`}
果然也不报错, 此时子组件中有 hook, 但改组件形式 <ComName />
调用, 而不是函数形式 tabList[key]()
调用.
然后再回看文档:
不要在常规 JavaScript 函数中调用 Hook 。 相反,你可以:在 React 函数式组件中调用 Hooks 。
感觉似乎明白了什么, 也验证了什么.
总结
- 不要在常规 js 函数中调用 hook
- 看来 jsx 不是完全兼容 js 的(或者说我认为 jsx 中
ComName = tabList[key]; <ComName />
与{tabList[key]()}
应该设计为表现一致).
错误报告摘要
Warning: React has detected a change in the order of Hooks called by com. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useState useState
2. useEffect useEffect
3. undefined useState
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
in com
in div
Uncaught Error: Rendered more hooks than during the previous render.
at updateWorkInProgressHook (react-dom.development.js:15115)
at updateReducer (react-dom.development.js:15173)
at updateState (react-dom.development.js:15372)
at Object.useState (react-dom.development.js:16084)
at useState (react.development.js:1583)
at Object.com [as Headers] (<anonymous>:101:21)
at <anonymous>:153:59
at Array.map (<anonymous>)
at com (<anonymous>:147:29)
at renderWithHooks (react-dom.development.js:14938)
The above error occurred in the <com> component:
in com
in div
Uncaught (in promise) Error: Rendered more hooks than during the previous render.
at updateWorkInProgressHook (react-dom.development.js:15115)
at updateReducer (react-dom.development.js:15173)
at updateState (react-dom.development.js:15372)
at Object.useState (react-dom.development.js:16084)
at useState (react.development.js:1583)
at Object.com [as Headers] (<anonymous>:101:21)
at <anonymous>:153:59
at Array.map (<anonymous>)
at com (<anonymous>:147:29)
at renderWithHooks (react-dom.development.js:14938)