跳到主要内容

第 17 章:19 新 Hooks——use / useOptimistic / useActionState / useFormStatus / useEffectEvent

React 19 带来了一批新 hook。它们不是把旧 hook 换个写法,而是补上了三类能力:渲染期读异步数据(use)、表单与 action 的状态管理(useActionState / useFormStatus / useOptimistic)、渲染期之外却需要稳定引用的回调(useEffectEvent)。

先记住这一句:19 的新 hook 分三族:use 让你在渲染时读 promise 和 context;表单三件套管 action 的 state、status 和乐观更新;useEffectEvent 给 effect 一个"永远最新又不引发重跑"的回调。

一图流:19 新 hook 分三族​

use:渲染期直接读 promise / context​

use(ReactFiberHooks.js:1151)是 19 最"破格"的 hook:它可以在条件语句、循环里调用,不用遵守第 13 章的 hook 顺序规则。原因很简单:它不占 hook 链表节点,而是用一个独立的 thenableState 记录。

const data = use(fetchData()); // 挂起,Suspense 显示 fallback
const theme = use(ThemeContext); // 读 context,等价 readContext

源码里 use 就是类型分发(1486):

function use(usable) {
if (usable !== null && typeof usable === 'object') {
if (typeof usable.then === 'function') {
// 是 thenable(promise)→ 挂起路径
return useThenable(usable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
// 是 context → 等价 readContext
const context = usable;
return readContext(context);
}
}
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}

传 promise 时走 useThenable(1504):把 promise 和当前 fiber 一起加入 thenableState(一个按 fiber 分组的 map),然后throw promise。这个 throw 被外层 Suspense 的 error boundary 捕获(第 27 章),Suspense 显示 fallback。promise 落定后,React 重新渲染,use 再次调——这次 promise 已 settled,直接 return 值。

function useThenable(thenable) {
const result = trackUsedThenable(thenable); // 检查是否已 settled
switch (result.status) {
case 'fulfilled': return result.value; // 已落定,直接返回
case 'rejected': throw result.reason; // 已拒绝,抛错给 error boundary
default:
// pending → 挂起
suspendThisFiberOnThenable(thenable); // 抛 promise 挂起
}
}

不占 hook 链表所以可条件调用。Suspense 的机制(第 27 章)就是靠它把"等异步"接进渲染。

表单与 action 三件套​

这三个 hook 是配套 <form action> 用的(第 32 章),内部都跟"action 的异步状态"打交道。

useActionState(mountActionState):把 action 包装成"带 state 的版本"。它的实现里其实藏了两个 state hook:

function mountActionState(action, initialStateProp, permalink) {
// 1. 一个 state hook 存 action 的返回值(form state)
const stateHook = mountWorkInProgressHook();
stateHook.memoizedState = stateHook.baseState = initialState;

// 2. 另一个 state hook 存 pending 状态
const pendingStateHook = mountWorkInProgressHook();
pendingStateHook.memoizedState = pendingStateHook.baseState = false;

// 把两个 hook 的 queue 存起来
hook.queue = { lastRenderedState: initialState, pending: false, ... };

// 3. 包装 action:调用前后自动设 pending
const dispatch = (queue.dispatch = dispatchActionState.bind(
null, currentlyRenderingFiber, queue, action,
));

return [initialState, dispatch, false]; // [state, action, pending]
}

注意它占两个 hook 链表节点:一个存 state、一个存 pending。所以组件里用 useActionState 时,React 的 hook 顺序计数是多跳一步的。dispatchActionState 在 action 执行前后自动调 dispatchOptimisticSetState 翻转 pending(2384),action 跑完 state 更新,组件重渲染。

useFormStatus(ReactDOMFormActions.js:67)是所有 hook 里"最短"的一个,就两行:

export function useFormStatus(): FormStatus {
const dispatcher = resolveDispatcher();
return dispatcher.useHostTransitionStatus();
}

它只是把调用转发给 dispatcher 的 useHostTransitionStatus。useHostTransitionStatus(ReactFiberHooks.js:3475)的实现就是读 context:

function useHostTransitionStatus() {
const status = readContext(HostTransitionContext);
return status;
}

而 HostTransitionContext 是由 <form> 组件通过 Provider 写入的。<form action={...}> 在渲染时把当前 pending 状态写进这个 context,useFormStatus 通过 readContext 读出来。它不需要参数,因为它读的永远是"离我最近的 form 的 Provider",这就是 React context 的"就近读取"特性。

useOptimistic(mountOptimistic):乐观更新。它基于第 14 章那个双缓存差异:

function mountOptimistic(passthrough) {
const hook = mountWorkInProgressHook();
hook.memoizedState = hook.baseState = passthrough; // 初始值 = 传入值
const queue = {
pending: null,
lanes: NoLanes,
// dispatch 就是特殊的 setState
dispatch: dispatchOptimisticSetState.bind(null, currentlyRenderingFiber, queue),
};
hook.queue = queue;
return [passthrough, queue.dispatch]; // [乐观值, setOptimistic]
}

更新时 updateOptimistic(1984):

function updateOptimistic(passthrough) {
const hook = updateWorkInProgressHook();
// 先跑 updateReducer,处理 setOptimistic 塞进来的 update
const [newState] = updateReducer(basicStateReducer);
// 然后把 baseState 设回到 passthrough(服务器确认的值)
hook.baseState = passthrough;
return [hook.memoizedState, queue.dispatch];
}

关键在这里:memoizedState 是跑完 updateReducer 后的"乐观值",baseState 是 passthrough(服务器确认的值)。调用 setOptimistic 时 dispatchOptimisticSetState(dispatchOptimisticSetState)立刻在 memoizedState 上改——界面秒变。action 完成后 passthrough 更新,baseState 对齐回服务器真值。这就是"先改界面、等服务器确认后修正"的底层。

useEffectEvent:永远最新的稳定回调​

useEffectEvent(源码里叫 mountEvent,2723)解决的是 effect 闭包的老难题。它的实现把回调存在 ref = {impl: callback} 里:

const ref = {impl: callback};
hook.memoizedState = ref;
return function eventFn() {
return ref.impl.apply(undefined, arguments); // 每次都读最新的 impl
};

eventFn 引用稳定(deps 数组里永远一样),但调用时通过 ref 读最新的回调,所以不会拿到旧闭包。这就绕开了"把函数加进 effect deps 导致无限重跑"的死结。它还带一道保险:渲染期间调用直接抛错("A function wrapped in useEffectEvent can't be called during rendering."),逼你把副作用挪到正确时机。

动手实验​

  1. use 读 promise:const p = fetchData(); <Suspense fallback={...}><Comp/></Suspense>,在 use(ReactFiberHooks.js:1151)打断点,看它怎么把 promise 挂到 Suspense 上。
  2. useFormStatus 转发:在 useFormStatus(ReactDOMFormActions.js:67)打断点,看它怎么转发到 useHostTransitionStatus,以及返回值 {pending, data} 在 form 提交时的变化。
  3. useEffectEvent 最新值:一个 5 秒的 interval,每秒调 eventFn(),中途改 props,观察它是不是永远读到最新值(对照源码里 ref.impl)。

小结​

  • use:渲染期读 promise / context,不占 hook 链表,所以可条件调用。
  • useActionState:action + state + pending 三合一,内部是两个 state hook。
  • useFormStatus:转发到 useHostTransitionStatus,读最近 form 的 status。
  • useOptimistic:利用 base / memoized 双缓存差异做乐观更新。
  • useEffectEvent:ref.impl 永远最新,回调引用稳定,渲染期调用会报错。
  • 第三部分到此收尾。下一部分 commit 阶段,看这些 hook 挂上去的 effect 怎么被落地的。