React 19 新特性之useFormStatus
- Published on
- Reading time
- 9 min read
- Likes
背景说明
在 React 18/19 里,随着 Server Actions 和 Form 组件等功能的引入,React 官方为"表单状态管理"提供了更直接的支持。useFormStatus 是在这个背景下诞生的一个新 HookuseFormStatus – React 中文文档useFormStatus – React 中文文档The library for web and native user interfaces,它专门用于处理以下场景:
- 表单提交过程中的加载状态展示
- 防止重复提交
- 提交过程中的用户反馈
- 在并发模式下的状态一致性保证
useFormStatus案例场景
为 “Server Actions” 场景提供更易用的状态管理,简化“局部加载指示”或“禁用按钮”等交互处理,并且这样做可以配合并发特性,避免手动处理竞态(race conditions)
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
)
}
在以往没有 Form 和 useFormStatus 的时候,我们常常需要:
- 在父组件或某个高层组件中,创建一个“isSubmitting”之类的 state。
- 在提交函数中更新它:setIsSubmitting(true),提交后 setIsSubmitting(false)。
- 在子组件中通过 props 或 Context 获取这个 isSubmitting,做加载指示或禁用表单等等。
这样做往往会导致:
- 在各组件之间不断传递这个状态(props drilling)。
- 多个表单时要维护多个 isSubmitting 状态,还得区分属于哪一个表单。
- 一旦有并发提交,逻辑就更加复杂。
而现在只要用 Form 把整个表单包裹起来,它就能捕获到这个“表单提交动作”,useFormStatus 在任何子组件中都能直接获取到相关状态,不需要手动提升状态。
竞态问题的解决React18React v18.0 – React BlogThis blog site has been archived. Go to react.dev/blog to see the recent posts. React 18 is now available on npm! In our last post, we shared step-by-step instructions for upgrading your app to React 18. In this post, we’ll give an overview of what’s new in React 18, and what it means for the future. Our latest major version includes out-of-the-box improvements like automatic batching, new APIs like startTransition, and streaming server-side rendering with support for Suspense. Many of the…
开始引入了并发模式,这意味着我们可以在同一时间内执行多个异步操作,这就可能导致竞态问题。useFormStatus 也能很好的解决这个问题,它会自动处理并发提交的情况,确保只有最后一个提交会被视为“pending”,并且在提交完成后才会重置状态。
没有并发模式下,通常我们可以这样做:
- 用户点击“提交” -> setIsSubmitting(true)。
- 等待异步请求完成 -> setIsSubmitting(false)。
如果用户非常快地再次点击了提交按钮(或者由于某些原因触发了二次提交),我们可能用一个“正在提交中就禁止再次提交”的逻辑去规避:
function MyForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (isSubmitting) return; // 正在提交中就返回
setIsSubmitting(true);
try {
// 假设这里是异步请求
await saveToServer(...);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<button disabled={isSubmitting}>提交</button>
</form>
);
}
在这种同步渲染的场景下,这段逻辑一般问题不大。
但是在并发模式下,可能出现以下特殊情况:
- React 发起一次渲染并把 isSubmitting 设为 true。
- 在这次渲染还没“commit”到界面之前,如果某些并发操作让 React 决定“中断、回退、重新开始渲染”,就可能导致组件的状态出现不一致。
- 同时,如果请求很快完成或者产生了错误,又触发了另一段更新,就可能出现渲染和状态管理上“谁先谁后”的冲突——有些更新被合并、有些被丢弃,最终导致 isSubmitting 的值和实际提交请求是否完成“错位”,出现竞态问题。
场景说明
你有一个页面上有两个交互:
- 一个 “提交” 按钮,用来切换 isSubmitting 的状态。
- 一个 “高优先级” 输入框,用来搜索或动态过滤列表。
- 当点击“提交”时,你会执行 setIsSubmitting(true),并且需要一点计算/网络请求时间去完成,然后再 setIsSubmitting(false)。
- 在 并发模式 下,React 可能会在“提交”的渲染还未完成时,插入“高优先级”操作(比如用户开始疯狂输入搜索关键词)。React 发现“用户输入”更紧急,就会打断/回滚“提交”相关的渲染,以先完成更高优先级的更新。
有了 useFormStatus 后如何避免这种竞态?
useFormStatus 之所以能够避免手动处理这种竞态问题,原因在于:
- React 会自动追踪当前这个 Form 的提交过程:不管在并发模式下有没有打断或重试,React 都知道当前这次提交请求是 属于哪一次 表单提交的。
- 当表单提交 pending 状态时,React 内部会设置一个挂起(suspense/pending)的标记;当提交完成、报错或被取消时,这个标记自动更新。
- useFormStatus 只是直接读取这个标记并返回给你一个 pending(以及将来的其他状态,例如 error)等布尔值/信息。
这样一来,无论 React 在内部如何处理并发、打断或重试,你拿到的 pending 状态总是“离最新的那个提交最接近的状态”。也就是说,React 官方帮你做了“只关心最新一次提交”的竞态管理工作。
用 useFormStatus 改写上述案例
在使用 Form + Server Actions + useFormStatus 的场景下,示例可以简化为:
'use client'
import { useFormStatus, Form } from 'react-dom' // 假设 import 路径
export default function MyPage() {
async function handleAction(formData) {
// 这里是服务器端的逻辑,比如数据库保存
await new Promise((r) => setTimeout(r, 1000)) // 模拟网络延迟
return { success: true }
}
return (
<Form action={handleAction}>
<MySubmitButton />
</Form>
)
}
function MySubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
)
}
在这里:
- Form 帮你捕获所有提交行为。
- useFormStatus 直接返回是否处于 pending 状态、是否有错误等信息。
- 如果用户狂点按钮(甚至在并发模式下出现渲染打断和重试),React 会自动帮我们追踪“哪一次是最新提交”,以及“提交是否还在进行中”。
- 我们只要用 disabled={pending} 把按钮灰掉就好了,不会出现需要手动写一堆逻辑来区分是哪一次提交,也不用担心 isSubmitting 和“真实提交状态”不一致的问题。
最佳实践
- useFormStatus 只能在 Form 组件内部使用
- 建议将状态展示组件(如提交按钮)单独抽离成组件
- 配合 React.Suspense 使用可以实现更好的加载体验
- 注意区分客户端组件和服务器组件的使用场景
本文采用CC BY-NC-SA 4.0 - 非商业性使用 - 相同方式共享 4.0 国际进行许可。