JavaScript / 前端工程 / React
React 19 Actions 实战:把提交、等待和乐观更新写清楚
表单提交看起来只是“拿到数据,发起请求,更新界面”,实际项目里却总会长出一串状态:isSubmitting 防止重复点击,error 展示业务错误,成功后清空输入,列表等待刷新,还要处理网络失败时的回滚。
React 19 把这类异步状态更新统一到 Actions 的思路下,并提供 useActionState、useOptimistic 等能力。它们不是替你设计接口,也不会让错误处理自动消失;真正的价值是让“提交动作”和它产生的等待、结果、乐观状态形成更清晰的生命周期。
先理解 React 里的 Action
在这里,Action 可以理解为一个会引发状态变化、可能包含异步副作用的函数。表单的 action 属性可以接收函数,React 会围绕它管理提交过程。React 19 官方发布说明也把 pending 状态、错误、表单处理和乐观更新作为这套模型的重要部分。
最简单的函数式表单如下:
1 | function SearchForm({ onSearch }) { |
传给函数的 FormData 来自表单中带 name 的控件。没有 name 的输入不会进入提交数据,这是从传统 HTML 表单延续下来的规则,也是实际开发中最常见的“为什么取不到值”原因之一。
用 useActionState 管理结果和等待状态
当 Action 需要返回校验错误或成功信息时,可以使用 useActionState:
1 | import { useActionState } from 'react'; |
它的签名可以概括为:
1 | const [state, formAction, isPending] = useActionState(action, initialState); |
最容易写错的是 Action 参数顺序。使用 useActionState 后,回调第一个参数是上一次状态,表单提交的 FormData 是后续参数。把函数仍写成 async function action(formData),你拿到的其实会是 previous state。
三个返回值各有明确职责:
state:最近一次 Action 返回的状态;formAction:应该交给<form action={...}>的新 Action;isPending:本次 Action 是否仍在执行。
这种模型比手动维护多个互相关联的布尔值更紧凑,但状态结构仍要克制。通常 status + message + 必要数据 就够了,不要把所有服务端响应原样塞进组件状态。
业务错误应返回,异常错误应进入错误边界
“邮箱已存在”属于用户可以理解并修正的业务结果,适合从 Action 返回并在表单附近展示。网络代码抛出的未知异常、程序错误或无法恢复的错误,则应交给错误边界和监控系统。
可以对预期错误做显式转换:
1 | async function updateProfile(previousState, formData) { |
不要用一个宽泛的 catch 把所有错误都变成“请重试”。那样会掩盖真正的代码缺陷,也让线上监控失去信号。
用 useOptimistic 提前展示结果
评论、点赞、待办事项这类操作,如果总要等网络返回后才变化,界面会显得迟钝。useOptimistic 允许我们在 Action 进行期间展示推测结果:
1 | import { useOptimistic } from 'react'; |
更新函数 (currentTodos, newTodo) => ... 应保持纯净:不要在里面发请求、改外部变量或生成依赖调用次数的副作用。临时 ID 在调用 addOptimisticTodo 前创建,更容易理解和测试。
乐观状态只在 Action 进行期间覆盖真实状态。Action 成功后,外部 todos 仍需要通过缓存更新、父组件状态或重新请求变成最新结果;否则乐观项会消失,看起来像“提交成功后反而回滚”。如果 Action 失败,乐观状态回退是合理的,但还必须告诉用户发生了什么,并保留重试入口。
哪些操作不适合乐观更新
乐观更新适合成功率高、结果容易预测、失败可恢复的操作,例如点赞、关注、排序或新增普通待办。下面这些场景则应更谨慎:
- 支付、转账和库存扣减;
- 权限审批等不可轻易撤销的动作;
- 服务端会生成大量不可预测结果的操作;
- 失败后用户可能基于错误状态继续做关键决策的流程。
“看起来更快”不能以展示虚假成功为代价。高风险操作可以先显示明确的处理中状态,等待服务端确认后再给出成功反馈。
Pending 状态不只是禁用按钮
isPending 最直观的用途是防止重复提交,但完整体验还应考虑:
- 按钮文案说明当前正在做什么,而不只是变灰。
- 使用
aria-live="polite"宣布成功或错误信息。 - 不要禁用整个表单到让用户无法复制已输入内容。
- 对耗时较长的操作提供取消或离开提示。
- 服务端接口仍要具备幂等或去重能力,前端禁用按钮不是数据安全保证。
如果提交按钮被拆成深层子组件,可以使用 useFormStatus 读取所在表单的 pending 状态。但它依赖表单上下文,调用它的组件必须位于目标 <form> 内部。不要为了少传一个 prop,就把本来清楚的组件边界变得隐晦。
从旧写法迁移时怎么做
旧表单常见结构是 onSubmit + preventDefault + 多个 useState。迁移不需要一次推翻所有逻辑,可以按下面的顺序进行:
- 把提交副作用提取成一个独立的 async 函数。
- 区分可展示的业务结果和应该抛出的异常。
- 用
useActionState接管结果状态与 pending 状态。 - 把表单改为函数
action,确认所有控件都有正确的name。 - 只有真实用户体验受网络等待影响时,再引入
useOptimistic。 - 为重复提交、失败回滚、慢请求和无障碍提示编写测试。
Actions 的重点不是少写几行代码,而是让异步状态变化拥有一致的结构:输入来自表单,等待状态跟随动作,业务结果明确返回,意外异常正常上抛,乐观结果有真实数据接管。只要边界清楚,复杂表单也不必退回一堆难以同步的布尔状态。
参考资料
- React 19 发布说明 — https://react.dev/blog/2024/12/05/react-19
- React:useActionState — https://react.dev/reference/react/useActionState
- React:useOptimistic — https://react.dev/reference/react/useOptimistic