, ,

React Hook 为什么不能写在循环、条件判断、嵌套函数里

React 靠 调用顺序 匹配 Hook 状态,顺序乱了,状态就会错乱、绑定错位。 核心原理:Hook 依靠「…

React 靠 调用顺序 匹配 Hook 状态,顺序乱了,状态就会错乱、绑定错位。

核心原理:Hook 依靠「调用顺序」存取状态

1. React 内部存储机制

每个函数组件,React 会维护一个线性链表 / 数组,专门存放当前组件所有 Hook 的状态(useState/useEffect/useRef 等)。

执行逻辑:

  1. 组件首次渲染:从上到下依次执行 Hook,按顺序把状态存入数组;
  2. 组件更新重渲染:再次从上到下执行 Hook,按完全相同的顺序从数组里取出对应状态。

关键:Hook 的位置 = 状态的索引,顺序一旦改变,索引就错位。

2. 举个极简模型

模拟 React 内部 Hook 存储数组 hookStates = [],用索引匹配:

function Demo() {
  // 第 1 个 Hook → 索引 0
  const [count, setCount] = useState(0);
  // 第 2 个 Hook → 索引 1
  const [name, setName] = useState('');

  return <div />;
}
  • 首次渲染:hookStates[0] = 0hookStates[1] = ''
  • 每次重渲染:永远先取索引 0,再取索引 1,状态完全匹配。

二、为什么循环 / 条件里用 Hook 会崩?

案例 1:Hook 写在 for 循环里

function List({ list }) {
  const elements = [];

  // 遍历数组,循环内部调用 useState
  for (let i = 0; i < list.length; i++) {
    // 问题:列表长度变化 → Hook 执行数量/顺序变化
    const [num, setNum] = useState(i);
    elements.push(<div key={i}>{num}</div>);
  }

  return <>{elements}</>;
}

问题推演:

  1. 初始 list.length = 2:执行 2 次 useState,内部数组存 2 个状态;
  2. 后续 list.length 变成 1:只执行 1 次 useState
  3. 重渲染时: 上次 2 个 Hook,这次只执行 1 个,索引对应关系彻底错乱,状态读取错误、组件异常、甚至报错。

案例 2:Hook 写在 if 条件里(分支导致数量变化 )

function Demo({ isShow }) {
  if (isShow) {
    // 条件成立才执行 Hook
    const [val, setVal] = useState(1); 
  }
  const [count] = useState(0);
  return <div />;
}

问题推演:

  1. isShow = true:执行顺序 useState(1)useState(0)(2 个 Hook)
  2. isShow = false:跳过第一个,只执行 useState(0)(1 个 Hook)
  3. 切换 isShow 后,Hook 数量、顺序改变,状态索引错位,直接触发异常。

案例 3:嵌套函数内部调用 Hook

function Demo() {
  function inner() {
    // 嵌套函数里用 Hook
    const [a] = useState(1);
  }
  inner();
  return <div />;
}

虽然没有循环 / 条件,但 React 规则同样禁止:框架设计上要求 Hook 必须在组件顶层作用域,嵌套调用会打乱执行链路,同时违背设计规范。

三、React Hook 官方两条硬性规则

  1. 只在函数组件 / 自定义 Hook 的顶层调用 Hook 不要在 if/elsefor/while、嵌套函数、三元表达式内部使用。
  2. 只在 React 函数中调用 Hook 不要在普通 JS 函数、类组件中使用。

四、循环场景如何替代

方案:把「循环内容抽成子组件

让每个子组件拥有自己独立的 Hook 状态,互不干扰。

1. 拆分子组件

// 子组件:每个 Item 独立使用 Hook
function Item({ initNum }) {
  const [num, setNum] = useState(initNum);
  return (
    <div>
      {num}
      <button onClick={() => setNum(num + 1)}>+</button>
    </div>
  );
}

// 父组件:只做循环渲染,不写 Hook
function List({ list }) {
  return (
    <>
      {list.map((item, index) => (
        <Item key={index} initNum={index} />
      ))}
    </>
  );
}

原理:

  • 每个 <Item> 都是独立组件,各自有一套独立的 Hook 状态数组;
  • 列表增删、长度变化,只会新增 / 销毁子组件,不会打乱父组件 Hook 顺序

备选方案:用单个状态存数组(不拆分子组件)

如果不想拆组件,可以用 useState 存一个数组,统一管理所有项的状态:

function List({ list }) {
  // 顶层单次调用 useState,存储整个列表状态
  const [nums, setNums] = useState(() => list.map((_, i) => i));

  const changeNum = (idx) => {
    const newArr = [...nums];
    newArr[idx] += 1;
    setNums(newArr);
  };

  return (
    <>
      {nums.map((num, idx) => (
        <div key={idx}>
          {num}
          <button onClick={() => changeNum(idx)}>+</button>
        </div>
      ))}
    </>
  );
}

五、和 Vue3 Hook 的区别

这也是很多人混淆的点:

  1. React Hook
    • 依赖固定调用顺序,严禁循环 / 条件嵌套;
    • 每次渲染函数完整重执行,靠索引匹配状态。
  2. Vue3 组合式 Hook(Composables)
    • 基于 Proxy 响应式 + 依赖收集,不依赖执行顺序;
    • 可以在循环、条件、嵌套函数里使用,语法限制宽松很多。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

About the Author

每个人都有自己得时区,在自己得时区里,一切都是准时的。

BlockSpare — News, Magazine and Blog Addons for (Gutenberg) Block Editor