第 31 章:Actions 与表单——useActionState 全流程
表单是 React 的老大难:提交、加载中、失败回滚、乐观更新,全靠手写 useState + useEffect 拼。React 19 用 Actions 把这套做成内建能力。第 16 章看过 useActionState/useFormStatus/useOptimistic 的注册,本章看它们怎么配合起来跑完一次表单提交。
先记住这一句:Actions 让「异步提交」成为一等公民:
<form action>触发,useActionState管 state + pending,useFormStatus读状态,useOptimistic做乐观更新。三件套共享"这次 action 进行到哪"这条线。
一图流:一次表单提交的分工
form action:触发点
<form action={fn}> 是原生行为的增强:表单提交不再走"受控组件 + 事件 + 手动 fetch",而是直接调 fn(fn 可以是个 Server Action,第 30 章的 use server)。提交动作会被识别成"一次 action 执行",React 负责调度它、跟踪它的状态、并把结果广播给所有相关 hook(entangleAsyncAction,ReactFiberAsyncAction.js:71 负责把 action 和它的 thenable 纠缠到 React 的调度里)。
useActionState:action 的 state + pending
mountActionState(ReactFiberHooks.js:2356)内部其实塞了两个 state hook(第 16 章埋的伏笔):
const stateHook = mountWorkInProgressHook(); // 存 action 的返回值
stateHook.memoizedState = initialState;
stateHook.queue = stateQueue; // 用 dispatchSetState 更新
const pendingStateHook = mountStateImpl(false); // 存 pending
// pending 走 dispatchOptimisticSetState:乐观地立刻翻转
所以 const [state, formAction, pending] = useActionState(action, init) 的三元组:state 是 action 返回的结果(服务端数据、错误信息),formAction 是包装好的提交函数,pending 是"这次提交进行中"的布尔。action 完成,state 更新、pending 归 false,组件重渲染。
useFormStatus:读"离我最近的表单"
useFormStatus 就是一行转发(ReactDOMFormActions.js:67):dispatcher.useHostTransitionStatus()。它读的是最近的 <form> 的状态,所以它不用传参,表单在哪一层,它就读哪一层的 {pending, data, method, action}。典型用法:提交按钮放在子组件里,父组件用 useActionState,按钮用 useFormStatus 读 pending 来禁用/转圈,不用把 pending 一层层传下去。
useOptimistic:先乐观,后对齐
useOptimistic(1949)做乐观更新:调用 setOptimistic(新值),立刻把 UI 改成"我预计会成功的样子";action 真正完成时,用服务端返回的真实值对齐。它吃第 13 章的双缓存差异:baseState 是"服务端确认的值",memoizedState 是"乐观假设的值",两个值差着的那段,就是 UI 提前显示的部分。
三件套的配合逻辑
提交按钮一按,一条线串起来:form action 触发 → entangleAsyncAction 把 action 挂进调度 → useFormStatus 读到 pending=true 挂 loading → useOptimistic 把列表先改成"已提交"的样子 → action 返回 → useActionState 的 state 拿到真实结果 → pending=false、乐观值对齐。你不用写一行 useEffect 管 loading,这是 Actions 和手写方案的本质区别。
动手实验
- 看 useActionState 的两个 hook:在
mountActionState(2356)打断点,数它挂了几个 hook(应该有两个:state + pending)。 - 看 useFormStatus 转发:在
useFormStatus(ReactDOMFormActions.js:67)打断点,确认它只转发到useHostTransitionStatus,返回值随表单提交变化。 - 看乐观对齐:
useOptimistic提交一个慢 action,观察 UI 先变(乐观值)再对齐(真实值),中间 base/memoized 差着。
小结
- Actions = 异步提交内建化,
<form action>触发,entangleAsyncAction(71) 挂进调度。 - useActionState(2356) 内部两个 state hook:返回值 + 乐观 pending。
- useFormStatus(67) 一行转发,读最近 form 的状态,不用传参。
- useOptimistic(1949) 用双缓存差异做"先乐观后对齐"。
- 三件套配合,省掉手写 useEffect 管 loading。
- 下一步:19 怎么处理 title、style、preload 这些文档级资源,第 32 章。