关于在 jsx 中使用 hook 遇到的一个问题及分析


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)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM