關於在 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