这是一道字节的面试题,我的朋友面试的时候遇到了这个问题。它是有关React hooks调用顺序和条件使用限制的深层次理解。我将为其解释原理。
展示报错
import { useEffect, useState } from 'react'
function App() {
const [a, setA] = useState(0)
if (a < 2) {
const [b, setB] = useState(0)
}
const [c, setC] = useState(0)
useEffect(() => {
}, [a])
return (
<div onClick={() => { setA(a + 1) }}>{a}</div>
)
}
export default App
初始化的页面
点击两次后页面消失,并返回报错
报错内容
App.jsx:11 React has detected a change in the order of Hooks called by App. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: react.dev/link/rules-…
Previous render Next render
- useState useState
- useState useState
- useState useEffect ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
翻译内容
App.jsx:11 React 检测到 App 调用 Hook 的顺序发生了变化。如果不修复,这将导致错误和错误。有关更多信息,请阅读 Hook 规则:react.dev/link/rules-…
解析原因
这和React的管理hooks的状态的方法有关。 React 内部通过单向链表(Linked List)的结构来管理 Hooks 的状态。这种设计是为了确保 Hook 在每次组件渲染时都能被正确识别和更新,同时保持调用顺序的一致性。
1. Hook 的链表结构
在函数组件首次渲染(或挂载)时,React 会创建一个 Hook 对象的单向链表,每个 Hook 按调用顺序依次存储在链表中。后续更新时,React 会遍历这个链表,按顺序读取或更新对应的状态。
2. 工作原理
a. 首次渲染(Mount)
-
组件首次调用
useState或其他 Hook 时,React 会创建一个 Hook 对象,并链接到链表中。function Component() { const [a, setA] = useState(0) // Hook 对象 1 const [b, setB] = useState(0) // Hook 对象 2 const [c, setC] = useState(0) // Hook 对象 3 // ... }- 链表结构:
Hook1(A) -> Hook2(B) -> null
- 链表结构:
-
React 将链表与组件关联,后续渲染时通过链表顺序匹配 Hook。
b. 更新渲染(Update)
-
组件重新渲染时,React 会按顺序遍历链表,读取对应的 Hook 状态。
function Component() { const [a, setA] = useState(0) // 读取链表第一个节点 const [b, setB] = useState(0) // 读取链表第二个节点 const [c, setC] = useState(0) // 读取链表第三个节点 // ... } -
如果 Hook 的调用顺序与链表不一致(如因条件语句跳过某个 Hook),React 会无法匹配到正确的节点,导致状态错乱。
当b消失时,链表上存的b错误的给c。这显然是不对的。React所以不会让你把hooks包括useState放进判断和循环语句中,这样会打破hooks链表读取的顺序。
3. 为什么React会用链表结构来管理Hooks的状态?
- 动态顺序追踪:链表可以按顺序记录 Hook 的依赖关系,无需预先分配固定内存。
- 高效更新:在组件多次渲染时,只需遍历链表即可匹配状态,时间复杂度为 O(n)。
- 惰性初始化:Hook 的状态可以延迟初始化(如
useState(initializer)),链表支持按需计算。
总结
Hook第一次渲染会被React放进一个链表结构中,Hook和每一个节点按顺序一一对应。后续渲染中,每次hook都会按顺序一一读取链表。如果突然hook少一个,后面的hook就会一一错误的读取到不对应的数据。所以React不允许hooks放在判断循环语句。
建议把Hook放在组件的顶端。
function MyComponent() {
const [count, setCount] = useState(0); // ✅ 顶层
useEffect(() => {}); // ✅ 顶层
// ...其他逻辑
}