
一个函数就是一个动作
function Comment() { async function post(formData) { await save(formData.get('text')) }
return ( <form action={post}> <textarea name="text" /> <button type="submit">发布</button> </form> )}<form> 的 action 现在除了 URL,也可以收一个函数。这个函数会拿到提交的 FormData,可以异步。
三件由 React 替你做的事:提交跑在一次过渡里(所以不用写 e.preventDefault());动作成功后,表单里非受控字段会自动重置;HTTP 方法固定为 POST,不再看你写的 method。
注意最后一条的边界:自动重置的是非受控字段。受控字段的值来自 state,重不重置由你决定。
与 onSubmit 的取舍
官方在 <form> 参考页里把两者对比列了出来:
onSubmit 在所有版本都能用,能直接拿到提交事件,可以 preventDefault 后自己读数据,new FormData(e.target) 就够。
用 action 传函数则会额外得到:React 跟踪的提交中状态、抛出的错误自动送到最近的错误边界、可以配合 useActionState 与 useOptimistic,以及支持 Server Function。
一句话:只是想在提交时跑段代码,onSubmit 就够;要让 React 管「提交中」这件事,或者要让服务端参与,用 action。
一个按钮一个动作
表单默认动作用 action,某个按钮想另走一套,就在按钮上写 formAction:
<form action={publish}> <textarea name="content" /> <button type="submit" name="button" value="submit">发布</button> <button formAction={saveDraft}>存草稿</button></form>按钮的 name 与 value 也会进 FormData,所以动作函数里能知道用户点的是哪个按钮。
提交中的状态
按钮要显示「提交中」,用 useFormStatus:
function Submit() { const { pending } = useFormStatus() return <button type="submit" disabled={pending}>{pending ? '提交中……' : '提交'}</button>}它必须写在表单内部的子组件里,所以在渲染表单的那个组件里直接调用是拿不到状态的。
错误怎么处理
动作函数抛错时,把表单包在错误边界里,让边界显示兜底界面:
<ErrorBoundary fallback={<p>提交失败,请重试</p>}> <form action={submit}> {/* ... */} </form></ErrorBoundary>可预期的错误(校验没过、邮箱重复)不该抛,应该 return 成状态显示在界面上,那部分交给 useActionState。
让服务端参与
把 action 换成一个 Server Function(标了 'use server' 的函数),表单在 JavaScript 还没加载时也能提交,这就是渐进增强。传参有两种写法:用隐藏字段放进表单,或者用 .bind(null, productId) 预先绑一个参数。
这一层通常由框架铺好,业务代码里直接写 action={serverAction} 即可。
四条边界
action 里的数据只在提交那一刻取。需要在输入过程中就做校验或预览,那属于受控组件的活,见 受控组件与非受控组件。
方法是 POST,别指望用函数形式的 action 做 GET 语义的搜索表单。
表单项都要有 name,否则不会进 FormData。
同一时刻别让多个动作改同一份状态:useActionState 的队列是串行的,连点会依次排队执行。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 190 天
部分内容可能已过时