React中useState不能放在判断和循环语句中

246 阅读3分钟

这是一道字节的面试题,我的朋友面试的时候遇到了这个问题。它是有关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


初始化的页面

image.png 点击两次后页面消失,并返回报错

image.png

报错内容

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

  1. useState useState
  2. useState useState
  3. 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)
  1. 组件首次调用 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
  2. React 将链表与组件关联,后续渲染时通过链表顺序匹配 Hook。

b. 更新渲染(Update)
  1. 组件重新渲染时,React 会按顺序遍历链表,读取对应的 Hook 状态。

    function Component() {
      const [a, setA] = useState(0) // 读取链表第一个节点
      const [b, setB] = useState(0) // 读取链表第二个节点
      const [c, setC] = useState(0) // 读取链表第三个节点
      // ...
    }
    
  2. 如果 Hook 的调用顺序与链表不一致(如因条件语句跳过某个 Hook),React 会无法匹配到正确的节点,导致状态错乱。

    当b消失时,链表上存的b错误的给c。这显然是不对的。React所以不会让你把hooks包括useState放进判断和循环语句中,这样会打破hooks链表读取的顺序。 image.png

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(() => {});                   // ✅ 顶层
  // ...其他逻辑
}