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)