React Hooks 为什么不能写在判断里

以下 react 源码版本为 18.3.0

核心原因

React hooks 使用链表存储状态,依赖调用顺序。如果 hooks 在条件语句中调用,会导致调用顺序不一致,从而引发状态错乱。

关键源码位置

1. Hooks 的链表结构定义

148:154:packages/react-reconciler/src/ReactFiberHooks.new.js

js 复制代码
export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
|};

每个 Hook 通过 next 指针形成链表。

2. Hooks 如何通过链表存储

189:194:packages/react-reconciler/src/ReactFiberHooks.new.js

js 复制代码
// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;

注释说明:Hooks 以链表形式存储在 fiber 的 memoizedState 字段上。

如何理解 currentHook 和 workInProgressHook

3. 首次渲染时创建 Hook 节点

636:655:packages/react-reconciler/src/ReactFiberHooks.new.js

js 复制代码
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

首次渲染时,hooks 按调用顺序依次添加到链表末尾。

4. 更新时按顺序遍历链表

657:716:packages/react-reconciler/src/ReactFiberHooks.new.js

js 复制代码
function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base. When we reach the end of the base list, we must switch to
  // the dispatcher used for mounts.
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.

    if (nextCurrentHook === null) {
      throw new Error('Rendered more hooks than during the previous render.');
    }

    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

更新时,React 通过 currentHook.next 按顺序遍历链表。如果某个 hook 被条件跳过,会导致:

  • 第 692 行:如果 nextCurrentHook === null,抛出 "Rendered more hooks than during the previous render"
  • 第 488-489 行:如果还有未遍历的 hooks,会检测到 "Rendered fewer hooks"

5. 错误检测和提示

269:314:packages/react-reconciler/src/ReactFiberHooks.new.js

js 复制代码
function warnOnHookMismatchInDev(currentHookName: HookType) {
  if (__DEV__) {
    const componentName = getComponentNameFromFiber(currentlyRenderingFiber);
    if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
      didWarnAboutMismatchedHooksForComponent.add(componentName);

      if (hookTypesDev !== null) {
        let table = '';

        const secondColumnStart = 30;

        for (let i = 0; i <= ((hookTypesUpdateIndexDev: any): number); i++) {
          const oldHookName = hookTypesDev[i];
          const newHookName =
            i === ((hookTypesUpdateIndexDev: any): number)
              ? currentHookName
              : oldHookName;

          let row = `${i + 1}. ${oldHookName}`;

          // Extra space so second column lines up
          // lol @ IE not supporting String#repeat
          while (row.length < secondColumnStart) {
            row += ' ';
          }

          row += newHookName + '\n';

          table += row;
        }

        console.error(
          'React has detected a change in the order of Hooks called by %s. ' +
            'This will lead to bugs and errors if not fixed. ' +
            'For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks\n\n' +
            '   Previous render            Next render\n' +
            '   ------------------------------------------------------\n' +
            '%s' +
            '   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',
          componentName,
          table,
        );
      }
    }
  }
}

当检测到 hooks 顺序变化时,会输出对比表格。

6. 检查 hooks 数量是否匹配

486:533:packages/react-reconciler/src/ReactFiberHooks.new.js

js 复制代码
  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;

  if (__DEV__) {
    currentHookNameInDev = null;
    hookTypesDev = null;
    hookTypesUpdateIndexDev = -1;

    // Confirm that a static flag was not added or removed since the last
    // render. If this fires, it suggests that we incorrectly reset the static
    // flags in some other part of the codebase. This has happened before, for
    // example, in the SuspenseList implementation.
    if (
      current !== null &&
      (current.flags & StaticMaskEffect) !==
        (workInProgress.flags & StaticMaskEffect) &&
      // Disable this warning in legacy mode, because legacy Suspense is weird
      // and creates false positives. To make this work in legacy mode, we'd
      // need to mark fibers that create in an incomplete state, somehow. For
      // now I'll disable the warning that most of the bugs that would trigger
      // it are either exclusive to concurrent mode or exist in both.
      (current.mode & ConcurrentMode) !== NoMode
    ) {
      console.error(
        'Internal React error: Expected static flag was missing. Please ' +
          'notify the React team.',
      );
    }
  }

  didScheduleRenderPhaseUpdate = false;
  // This is reset by checkDidRenderIdHook
  // localIdCounter = 0;

  if (didRenderTooFewHooks) {
    throw new Error(
      'Rendered fewer hooks than expected. This may be caused by an accidental ' +
        'early return statement.',
    );
  }

第 488-489 行检查是否还有未遍历的 hooks(didRenderTooFewHooks),如果存在则说明本次渲染调用的 hooks 数量少于上次。

总结

  • Hooks 以链表存储,依赖调用顺序。
  • 条件调用会导致顺序不一致,链表遍历错位,状态错乱。
  • React 在开发和生产模式下都会检查 hooks 数量与顺序,不一致会抛出错误。

因此,hooks 必须在函数组件的顶层调用,不能在条件语句、循环或嵌套函数中调用。

相关推荐
Mintopia8 小时前
⚛️ React 17 vs React 18:Lanes 是同一个模型,但跑法不一样
前端·react.js·架构
玉木成琳9 小时前
Taro + React + @nutui/nutui-react-taro 时间选择器重写
前端·react.js·taro
2401_860494709 小时前
在React Native中实现鸿蒙跨平台开发中开发一个运动类型管理系统,使用React Navigation设置应用的导航结构,创建一个堆栈导航器
react native·react.js·harmonyos
2301_796512529 小时前
使用状态管理、持久化存储或者利用现有的库来辅助React Native鸿蒙跨平台开发开发一个允许用户撤销删除的操作
javascript·react native·react.js
零Suger9 小时前
React 组件通信
前端·react.js·前端框架
2301_796512529 小时前
React Native鸿蒙跨平台开发包含输入收入金额、选择收入类别、记录备注和日期等功能,实战react-native-paper组件
javascript·react native·react.js
AI分享猿10 小时前
新手跨境电商实测:Apache 搭站,雷池 WAF 零基础部署
安全·web安全·react.js·网络安全·开源·apache
2401_8603195210 小时前
react-native-calendarsReact Native库来帮助你处理日期和时间,实现鸿蒙跨平台开发日历组件
react native·react.js·harmonyos