Skip to content

第8章 React 19 新 Hooks 与 API

本章要点

  • use Hook 的革命性设计:在条件语句和循环中调用的第一个 Hook
  • useActionState 的工作机制:表单状态与异步 Action 的桥梁
  • useFormStatus 的实现原理:跨组件读取表单提交状态
  • useOptimistic 的乐观更新模型:即时反馈与最终一致性
  • Actions 的内核机制:Transition 与异步函数的融合
  • ref 作为 prop 的变革:forwardRef 的终结
  • React 19 API 变更的源码级解读

React 19 是自 Hooks 引入以来最重大的一次 API 更新。它不仅增加了 useuseActionStateuseFormStatususeOptimistic 等新 Hook,还从根本上改变了 React 处理异步操作和表单交互的方式。

如果说 React 16.8 的 Hooks 解决了"函数组件如何拥有状态"的问题,那么 React 19 的新 API 则瞄准了一个更大的目标:如何优雅地处理数据变更(mutation)。在此之前,React 一直擅长的是数据展示——从 state 到 UI 的单向流动。而数据的写入、提交、乐观更新等操作,长期以来都需要开发者自行搭建脚手架。React 19 将这些模式内建到了框架核心中。

8.1 use:打破 Hook 规则的 Hook

use 是 React 19 引入的最具颠覆性的 API。它打破了 Hooks 系统自诞生以来最核心的规则——它可以在条件语句和循环中被调用。

8.1.1 use 的两种模式

use 有两种截然不同的使用方式:读取 Promise 和读取 Context。

typescript
// 模式 1:读取 Promise
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
  // 如果 Promise 还没 resolve,会挂起组件(触发 Suspense)
  const comments = use(commentsPromise);
  return comments.map(c => <p key={c.id}>{c.body}</p>);
}

// 模式 2:读取 Context(可以在条件语句中)
function ThemeButton({ showIcon }: { showIcon: boolean }) {
  if (showIcon) {
    const theme = use(ThemeContext); // ✅ 在条件中调用 use
    return <Icon color={theme.primary} />;
  }
  return <button>Click me</button>;
}

8.1.2 use(Promise) 的内核实现

use 接收一个 Promise 时,它的行为与 Suspense 紧密耦合:

typescript
function use<T>(usable: Usable<T>): T {
  if (usable !== null && typeof usable === 'object') {
    if (typeof (usable as Thenable<T>).then === 'function') {
      // Promise 路径
      const thenable = (usable as Thenable<T>);
      return useThenable(thenable);
    } else if ((usable as ReactContext<T>).$$typeof === REACT_CONTEXT_TYPE) {
      // Context 路径
      const context = (usable as ReactContext<T>);
      return readContext(context);
    }
  }
  throw new Error('An unsupported type was passed to use()');
}

useThenable 是核心实现,它实现了"同步化异步"的魔法:

typescript
function useThenable<T>(thenable: Thenable<T>): T {
  const index = thenableIndexCounter;
  thenableIndexCounter += 1;

  if (thenableState === null) {
    thenableState = createThenableState();
  }

  const result = trackUsedThenable(thenableState, thenable, index);

  // 如果 result 是 SUSPENDED_THENABLE,说明 Promise 还没 resolve
  // React 会 throw 这个 thenable,触发 Suspense 边界
  if (
    currentlyRenderingFiber.alternate === null &&
    (workInProgressHook === null
      ? currentlyRenderingFiber.memoizedState === null
      : workInProgressHook.next === null)
  ) {
    // 初次渲染时 Promise 未完成:这是合法的 Suspense 场景
  }

  return result;
}

function trackUsedThenable<T>(
  thenableState: ThenableState,
  thenable: Thenable<T>,
  index: number
): T {
  const trackedThenables = thenableState;
  const previous = trackedThenables[index];

  if (previous === undefined) {
    // 第一次遇到这个 thenable
    trackedThenables[index] = thenable;

    switch (thenable.status) {
      case 'fulfilled':
        return thenable.value;
      case 'rejected':
        throw thenable.reason;
      default:
        // pending 状态:附加 then 回调
        const pendingThenable = thenable as PendingThenable<T>;
        pendingThenable.status = 'pending';

        pendingThenable.then(
          (fulfilledValue) => {
            if (thenable.status === 'pending') {
              const fulfilledThenable = thenable as FulfilledThenable<T>;
              fulfilledThenable.status = 'fulfilled';
              fulfilledThenable.value = fulfilledValue;
            }
          },
          (error) => {
            if (thenable.status === 'pending') {
              const rejectedThenable = thenable as RejectedThenable<T>;
              rejectedThenable.status = 'rejected';
              rejectedThenable.reason = error;
            }
          }
        );

        // 抛出 thenable,触发 Suspense
        throw thenable;
    }
  } else {
    // 之前已经追踪过
    switch (previous.status) {
      case 'fulfilled':
        return (previous as FulfilledThenable<T>).value;
      case 'rejected':
        throw (previous as RejectedThenable<T>).reason;
      default:
        // 仍在 pending,继续挂起
        throw previous;
    }
  }
}

图 8-1:use(Promise) 的挂起与恢复流程

8.1.3 为什么 use 可以在条件语句中调用

传统 Hook 不能在条件语句中调用,因为它们依赖 Hook 链表的顺序索引(见第 7 章 7.9 节)。use 之所以能突破这个限制,是因为它使用了完全不同的存储机制

typescript
// 传统 Hook:通过链表顺序索引
// Hook 1 → Hook 2 → Hook 3
// 每次渲染必须以相同顺序遍历链表

// use(Context):直接读取 Context 的 _currentValue
// 不需要链表,不依赖调用顺序

// use(Promise):通过 thenableState 数组 + index 追踪
// index 基于 use 在当前渲染中的调用计数,而不是所有 Hook 的调用计数

对于 use(Context),它直接调用 readContext,与 useContext 的实现完全相同——readContext 本身就不依赖 Hook 链表(见第 7 章 7.8 节)。

对于 use(Promise),它使用独立的 thenableState 数组和 thenableIndexCounter,与 Hook 链表是分离的追踪系统。这意味着即使 use 在条件分支中被调用了不同次数,也不会影响 Hook 链表的完整性。

8.2 Actions:异步 Transition 的进化

React 19 引入了 Actions 概念——将异步函数传递给 startTransition,使其成为一个可追踪状态的"Action"。

8.2.1 Transition 的异步扩展

在 React 18 中,startTransition 只接受同步回调。React 19 扩展了它的能力:

typescript
// React 18:只能同步
startTransition(() => {
  setSearchQuery(input); // 同步状态更新
});

// React 19:支持异步函数(Action)
startTransition(async () => {
  const data = await submitForm(formData);  // 异步操作
  setResult(data);                           // 异步完成后更新状态
});

内核实现中,当 startTransition 检测到回调返回了 Promise 时,React 会追踪这个 Promise 的生命周期:

typescript
function startTransition(
  fiber: Fiber,
  queue: UpdateQueue<boolean>,
  pendingState: boolean,
  finishedState: boolean,
  callback: () => void | Promise<void>
) {
  const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(
    higherEventPriority(previousPriority, ContinuousEventPriority)
  );

  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = {};

  // 先标记 isPending = true
  dispatchSetState(fiber, queue, pendingState);

  try {
    const returnValue = callback();

    if (
      returnValue !== null &&
      typeof returnValue === 'object' &&
      typeof returnValue.then === 'function'
    ) {
      // 🔑 这是一个 async Action
      const thenable = (returnValue as Thenable<void>);

      // 创建一个 "listener",在 Promise resolve 时标记 isPending = false
      const thenableForFinishedState = chainThenableValue(
        thenable,
        finishedState
      );

      // 通知 Transition 追踪系统
      entangleAsyncAction(fiber, thenableForFinishedState);
    } else {
      // 同步 Action,直接标记完成
      dispatchSetState(fiber, queue, finishedState);
    }
  } catch (error) {
    // Action 出错,也标记完成(isPending = false)
    dispatchSetState(fiber, queue, finishedState);
    throw error;
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}

基于 VitePress 构建