React 19 useActionState + useFormStatus 实战:表单提交的 pending、错误回显与乐观更新

发布时间:2026/8/8 11:39:57
React 19 useActionState + useFormStatus 实战:表单提交的 pending、错误回显与乐观更新 React 19 useActionState useFormStatus 实战:表单提交的 pending、错误回显与乐观更新写表单提交,你大概率手动维护过这三个 state:loading(防重复点击 转圈)、error(把后端返回的错误显示出来)、还有提交成功后清空或更新界面。三个useState加一个try/catch/finally,每个表单都抄一遍,又臭又长还容易漏。React 19 的useActionState和useFormStatus把这套模板收敛掉了。这篇从手写版的痛点讲起,一步步换成新 API。先看手写版有多啰嗦一个典型的「提交用户名」表单,老写法:import { useState } from react; function OldForm() { const [pending, setPending] useState(false); const [error, setError] useState(null); const [name, setName] useState(); async function handleSubmit(e) { e.preventDefault(); // 别忘了阻止默认提交 setPending(true); setError(null); try { const res await saveName(name); if (!res.ok) throw new Error(保存失败,换个名字试试); // 成功后的逻辑… } catch (err) { setError(err.message); // 手动接错误 } finally { setPending(false); // 无论成败都要复位 } } return ( form onSubmit{handleSubmit} input value{name} onChange{(e) setName(e.target.value)} / button disabled{pending}{pending ? 提交中… : 提交}/button {error p style{{ color: red }}{error}/p} /form ); }三个 state、一个preventDefault、一个try/catch/finally,还得记得在finally里复位pending。漏掉任何一环都是 bug。换成 useActionStateuseActionState(action, initialState)接收一个「action 函数」和初始状态,返回[state, formAction, isPending]:action 函数签名是(prevState, formData) newState:第一个参数是上一次的返回值,第二个是表单的FormData。state:action 的最新返回值,拿来做错误回显再合适不过。formAction:直接绑到form action{formAction}上。isPending:提交进行中,框架帮你维护,不用自己 setState。改造后:import { useActionState } from react; async function submitName(prevState, formData) { const name formData.get(name); // 直接从 FormData 取值 const res await saveName(name); if (!res.ok) { // 返回错误信息,连同用户刚填的值一起,方便回填 return { error: 保存失败,换个名字试试, name }; } return { error: null, name }; // 成功 } function NewForm() { const [state, formAction, isPending] useActionState(submitName, { error: null, name: , }); return ( form action{formAction} {/* 注意:input 不再需要受控,name 属性会进 FormData */} input namename defaultValue{state.name} / button disabled{isPending}{isPending ? 提交中… : 提交}/button {state.error p style{{ color: red }}{state.error}/p} /form ); }对比一下省了什么:不用preventDefault——用action{formAction}后 React 接管了提交,不会走浏览器默认跳转。不用自己维护pending——isPending自动在提交期间为true,结束自动false,连finally都省了。不用onChange受控——输入值通过name属性进入FormData,在 action 里formData.get(name)取。错误就是 action 的返回值,state.error直接读,不需要单独的 error state。useFormStatus:让子组件感知提交态上面isPending在NewForm里能拿到,但如果你的提交按钮是一个独立封装的组件呢?总不能层层透传isPending。useFormStatus让子组件直接读取「最近一个父级form的提交状态」:import { useFormStatus } from react-dom; // 注意是 react-dom 不是 react function SubmitButton({ children }) { const { pending } useFormStatus(); return ( button disabled{pending} {pending ? 提交中… : children} /button ); } // 用的时候什么都不用传: function NewForm() { const [state, formAction] useActionState(submitName, { error: null }); return ( form action{formAction} input namename / SubmitButton提交/SubmitButton {/* 自动感知 pending */} {state.error p style{{ color: red }}{state.error}/p} /form ); }两个坑提前说:useFormStatus从react-dom导入,不是react——记混了会报 undefined。它读的是「父级form」的状态,所以SubmitButton必须渲染在form内部;如果把它放在 form 外面,pending永远是false。进阶:useOptimistic 做乐观更新提交要等网络往返,界面转圈体验一般。useOptimistic让你先把「预期结果」立刻显示出来,等真实结果回来再对齐——就是发消息时那种「消息秒出、发送中小圈」的体验。import { useOptimistic, useActionState } from react; function TodoList({ todos }) { // optimisticTodos:界面实际渲染用的乐观列表 const [optimisticTodos, addOptimistic] useOptimistic( todos, (current, newText) [...current, { text: newText, sending: true }] ); const [, formAction] useActionState(async (_, formData) { const text formData.get(text); addOptimistic(text); // 立刻把新项塞进界面(标记 sending) await saveTodo(text); // 真实请求;完成后 React 用真实 todos 覆盖乐观值 return null; }, null); return ( form action{formAction} input nametext / button添加/button ul {optimisticTodos.map((t, i) ( li key{i} style{{ opacity: t.sending ? 0.5 : 1 }} {t.text} {t.sending (发送中…)} /li ))} /ul /form ); }addOptimistic(text)一调用,新项马上以半透明「发送中」出现在列表里;等saveTodo完成、父级传入新的todos,乐观状态自动被真实数据替换。如果请求失败(action 抛错),乐观更新会自动回滚到todos的原始值——不用你手动撤销。小结useActionState(action, init)一次性收掉 pending、error、提交逻辑三件套:返回[state, formAction, isPending],action 签名是(prevState, formData) newState。用form action{formAction}后,preventDefault、手动pending复位、受控 input 全省;输入值走name→FormData。useFormStatus让封装的子按钮感知父 form 提交态——从react-dom导入,且组件必须在 form 内部。useOptimistic做乐观更新:先渲染预期结果,成功后对齐、失败自动回滚。一句话记忆:useActionState 管「一个表单的状态机」,useFormStatus 管「子组件读提交态」,useOptimistic 管「先斩后奏的即时反馈」——三者组合,表单不用再手写 loading/error 模板。