跳到主要内容

useActionState 不防重复提交,它只排队

书签客
书签客

· 阅读约 4 分钟

这条讲 React 19 的 useActionState 实际行为,出处是 Shubhra Pokhariya。开头几段是 React 19 之前手动管表单三个状态的老账——结果、提交中、错误挨个维护,这是打过两年表单的人都知道的苦,没什么新鲜的,我本来想划走。读到慢速网络那个例子才停下来。

她讲的那个坑我熟,也踩过。手动管状态那套,每个表单都有 result、isPending、error 三个 useState,提交时置 isPending 为 true,请求回来再置 false。作者说她有一次忘了重置,按钮一直灰着,花了大概二十分钟才反应过来不是网络问题,是状态忘了翻。这种我太理解了——按钮一直 disabled,你盯着 Network 面板看请求回没回,回了,但按钮还是灰的,然后你才开始扒代码。二十分钟都算快的。

真正让我停下来的是慢速网络那段。本地布尔标志的毛病在于它跟真实请求生命周期脱钩:乐观地把 isPending 设成 false 那一刻,请求可能还在路上。慢速网络把这个窗口拉得很大。用户看到按钮短暂恢复可点击,又点了一下,然后可能再点一下。按我以前的理解,这时候两个请求会并发出去,把后端操翻了。React 19 不是这么做——它不丢这些点击,也不让它们竞争,而是排队。一个一个执行,一个都不少。连点三次 Place Order,就是三个订单,全算数。

这个排队语义我一开始不信,顺手跑了一下。

async function placeOrder(prev, formData) {
  await new Promise(r => setTimeout(r, 2000));
  console.log("order placed");
  return { ok: true };
}

function Form() {
  const [state, formAction, isPending] = useActionState(placeOrder, { ok: false });
  return (
    <form action={formAction}>
      <button disabled={isPending}>Place Order</button>
    </form>
  );
}

连点四次按钮。console 里四条,每条间隔两秒,总耗时八秒。不是并发,是队列。跟文章说的一致。

跑通那一刻我突然明白了一件事:useActionState 防不住重复提交。它干的事情是把并发变成串行,让每次点击都有结果,也都得等——它根本不去判断"这个动作该不该发生",只是把它排进队里。那个 disabled={isPending} 的写法乍一看像防重,实际挡的是用户手滑多塞一个动作进队列,不是挡"用户真的想点三次"。真防重归服务端。

所以看到评论区 Mustafa ERBAY 那条,我才觉得这讨论值。他说 useActionState 改善的是客户端协调和用户体验,不保证执行一次,服务端还是得要幂等键、唯一约束、原子事务。作者回得也干脆:她聚焦客户端 UX,幂等性属于 API 边界。这个边界划得很清楚——这个 hook 解决的是 UX 问题,不是 correctness 问题。你要是想用它替代服务端幂等,那方向从一开始就错了。

这条里还有两个坑,第二个我之前真没注意。

动作函数里用过时闭包变量——React 老毛病,换个皮又来一遍。action 闭包里的 state 可能不是最新值,作者说应该用 formData.get 读表单里的实时数据,别信闭包。这个我第一反应是"啊怎么还这样",但想想 action 在 transition 里调度,闭包捕获的时间点确实不保证跟你界面上看到的一致。

另一个是重置。useActionState 没有内置 reset 方法。作者给了两条路:让 action 识别一个重置信号,或者改 key 强制重新挂载。她明显更倾向 reset-signal,理由是重挂载会丢焦点、滚动位置,还有挂在上面的 useOptimistic 状态。就冲这个理由,我信她是真在表单里用过这个 API,不是看完文档写文章。重置信号那套虽然要自己在 action 里约定返回格式,但至少不炸掉焦点。

不点链接也能带走的一句:换成 useActionState 不代表你能省掉服务端的幂等键。想省,趁早别。

哦对,还有一点。文章末尾提到,像点赞按钮这种需要即时 UI 反馈的场景,作者建议用 useOptimistic 而不是 useActionState。理由很简单:useActionState 要等 action 解析完才更新状态,慢。点赞那一下等半秒才变红,用户会觉得卡。这个建议跟我跑实验时观察到的排队延迟是一回事——useActionState 换来的"每次都算数"的确定性,代价就是延迟。你要的是快,就得接受乐观更新和可能的不一致。这两个 hook 的取舍,作者几句话就讲透了。

书签客
书签客

只推真读过的、顺手跑个实验贴完整记录——link-blog 策展 + 实验笔记。

查看主页 →