useFormStatus
获取表单内子组件的提交状态
核心概述
在 React 19 之前,如果你想在表单按钮中显示"提交中..."状态, 必须通过 props 将 pending 状态从父组件传递到子组件,这导致:
- Props 穿透问题:需要在多个层级传递 pending 状态
- 组件耦合:子组件依赖父组件传递的状态
- 代码冗余:每个按钮组件都需要接收 pending prop
useFormStatus 是 React 19 引入的 Hook, 专门用于解决表单内子组件获取提交状态的问题。它会自动返回最近父级 <form> 的状态信息, 无需手动传递 props。
💡 心智模型
将 useFormStatus 想象成"表单状态探测器":
- • 自动向上查找:当你在按钮组件调用 useFormStatus 时, 它会自动向上查找最近的
<form>元素 - • 只读取状态,不修改:你只能查询表单是否正在提交、提交了什么数据, 不能修改表单行为
- • 实时更新:当用户点击提交,表单进入 pending 状态, useFormStatus 会立即返回新的状态,触发子组件重新渲染
- • 隔离作用域:每个 useFormStatus 只能访问自己所在的表单状态, 不会受到其他表单的影响
典型应用场景:
- 显示按钮的 loading 状态(禁用按钮 + 显示"提交中...")
- 显示提交进度或加载动画
- 根据提交状态禁用表单输入(防止重复提交)
- 访问提交的 FormData 数据(用于日志或预览)
技术规格
类型签名
function useFormStatus(): {
pending: boolean;
data: FormData | null;
method: 'GET' | 'POST' | undefined;
action: string | ((formData: FormData) => void) | undefined;
}返回值说明
| 属性 | 类型 | 说明 |
|---|---|---|
pending | boolean | 表单是否正在提交。true 表示表单提交中(等待 action 完成),false 表示表单空闲 |
data | FormData | null | 提交的表单数据。只在提交期间有值,提交完成后恢复为 null |
method | 'GET' | 'POST' | undefined | 表单的 HTTP 方法。由 <form method="..."> 属性决定 |
action | string | function | undefined | 表单的 action。可以是 URL 字符串或 action 函数 |
使用限制
- ⚠️ 必须在 <form> 内部使用:useFormStatus 必须在
<form>的子组件中调用, 否则返回的状态始终为初始值(pending: false, data: null) - ⚠️ 只能读取最近父表单的状态:如果嵌套多个表单, useFormStatus 只返回直接父级表单的状态
- ⚠️ 只能在客户端组件使用:必须在文件顶部添加
'use client'指令 - ⚠️ 不能在服务端组件使用:useFormStatus 依赖 React 客户端运行时
运行机制
useFormStatus 的实现依赖于 React 的上下文传播机制:
- 当你在组件中调用 useFormStatus 时,React 会向上遍历组件树查找最近的
<form>元素 <form>元素内部维护一个状态上下文(FormStatusContext), 存储 pending、data、method、action 等信息- 当表单提交时,React 会自动更新 FormStatusContext 的值,触发所有使用了 useFormStatus 的子组件重新渲染
- 子组件通过 useFormStatus 读取最新的状态信息,无需父组件手动传递 props
实战演练
示例 1: 基础按钮状态
显示按钮的 loading 状态,防止重复提交:
import { useFormStatus } from 'react-dom';
function SubmitButton() {
// 获取最近父表单的提交状态
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending} // 提交期间禁用按钮
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{pending ? '提交中...' : '提交'}
</button>
);
}
function ContactForm() {
const handleSubmit = async (formData: FormData) => {
// 模拟异步提交
await new Promise((resolve) => setTimeout(resolve, 2000));
};
return (
<form action={handleSubmit}>
<input name="email" type="email" placeholder="请输入邮箱" />
<SubmitButton /> {/* 无需传递 pending prop */}
</form>
);
}示例 2: 生产级表单(完整类型 + 错误处理)
结合 useActionState 和 useFormStatus 实现完整的表单状态管理:
'use client';
import { useFormStatus } from 'react-dom';
import { useActionState } from 'react';
// ============ 类型定义 ============
type FormState = {
success: boolean;
error: string | null;
errors: Record<string, string[]>;
};
// ============ Server Action ============
async function submitForm(
prevState: FormState,
formData: FormData
): Promise<FormState> {
// 模拟网络请求
await new Promise((resolve) => setTimeout(resolve, 1500));
const email = formData.get('email') as string;
// 简单验证
if (!email || !email.includes('@')) {
return {
success: false,
error: '请输入有效的邮箱地址',
errors: { email: ['邮箱格式不正确'] },
};
}
// 提交成功
return {
success: true,
error: null,
errors: {},
};
}
// ============ 提交按钮组件 ============
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="w-full py-2 px-4 bg-blue-600 text-white font-medium rounded-lg
hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-200"
>
{pending ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
提交中...
</span>
) : (
'订阅'
)}
</button>
);
}
// ============ 输入框组件 ============
function EmailInput() {
const { pending } = useFormStatus();
return (
<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
邮箱地址
</label>
<input
id="email"
name="email"
type="email"
required
disabled={pending} // 提交期间禁用输入框
className="w-full px-3 py-2 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-blue-500
disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="your@email.com"
/>
</div>
);
}
// ============ 主表单组件 ============
export default function SubscribeForm() {
const [state, formAction] = useActionState(submitForm, {
success: false,
error: null,
errors: {},
});
if (state.success) {
return (
<div className="p-6 bg-green-50 border border-green-200 rounded-lg">
<h3 className="text-green-800 font-semibold mb-2">订阅成功!</h3>
<p className="text-green-700">感谢您的订阅。</p>
</div>
);
}
return (
<form action={formAction} className="space-y-4 max-w-md">
<EmailInput />
{state.error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-800 text-sm">{state.error}</p>
</div>
)}
{state.errors.email && (
<p className="text-red-600 text-sm">{state.errors.email[0]}</p>
)}
<SubmitButton />
</form>
);
}示例 3: 访问提交数据
使用 data 属性访问表单提交的数据:
import { useFormStatus } from 'react-dom';
function FormDataLogger() {
const { pending, data } = useFormStatus();
// 只在提交期间显示数据
if (!pending || !data) {
return null;
}
// 读取 FormData 中的字段
const email = data.get('email');
const name = data.get('name');
return (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800 font-semibold mb-2">正在提交的数据:</p>
<ul className="text-sm text-blue-700 space-y-1">
<li>姓名: {name}</li>
<li>邮箱: {email}</li>
</ul>
</div>
);
}
function FormWithLogger() {
return (
<form action={submitAction}>
<input name="name" placeholder="姓名" />
<input name="email" placeholder="邮箱" />
<button type="submit">提交</button>
<FormDataLogger /> {/* 自动显示提交中的数据 */}
</form>
);
}示例 4: 嵌套表单的状态隔离
每个 useFormStatus 只能访问自己所在的表单状态:
import { useFormStatus } from 'react-dom';
function StatusDisplay({ formName }: { formName: string }) {
const { pending } = useFormStatus();
return (
<div className="text-sm">
{formName} 状态: {pending ? '提交中...' : '空闲'}
</div>
);
}
function FormA() {
return (
<form action={actionA} className="p-4 border">
<h3>表单 A</h3>
<StatusDisplay formName="表单 A" />
<button type="submit">提交表单 A</button>
</form>
);
}
function FormB() {
return (
<form action={actionB} className="p-4 border">
<h3>表单 B</h3>
<StatusDisplay formName="表单 B" />
<button type="submit">提交表单 B</button>
</form>
);
}
function NestedForms() {
return (
<div>
<FormA />
<FormB />
{/* 两个表单的状态完全独立 */}
</div>
);
}避坑指南
陷阱 1: 在 <form> 外部使用 useFormStatus
问题:如果 useFormStatus 不在任何 <form> 内部, 它将始终返回初始值(pending: false, data: null),无法获取表单状态。
后果:按钮永远不会显示 loading 状态,无法防止重复提交。
❌ 错误代码:
function SubmitButton() {
const { pending } = useFormStatus(); // 不在任何 form 内部!
return <button disabled={pending}>提交</button>;
}
function App() {
return <SubmitButton />; // ❌ 始终返回 { pending: false, data: null }
}✅ 修正代码:
function SubmitButton() {
const { pending } = useFormStatus(); // 在 form 内部
return <button disabled={pending}>提交</button>;
}
function App() {
return (
<form action={handleSubmit}>
<SubmitButton /> {/* ✅ 可以正确获取状态 */}
</form>
);
}心智模型纠正:useFormStatus 是"表单状态探测器", 必须放在"信号源"(form 元素)范围内才能接收到信号。
陷阱 2: 在服务端组件使用 useFormStatus
问题:useFormStatus 是客户端 Hook,只能在添加了'use client' 指令的组件中使用。
后果:运行时报错 "Error: useFormStatus is a client-only hook"。
❌ 错误代码:
// ❌ 缺少 'use client' 指令
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus(); // 运行时报错!
return <button disabled={pending}>提交</button>;
}✅ 修正代码:
// ✅ 添加 'use client' 指令
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus(); // 正常工作
return <button disabled={pending}>提交</button>;
}陷阱 3: 期望 useFormStatus 修改表单行为
问题:useFormStatus 只能读取表单状态, 不能修改表单行为(如阻止提交、修改 action)。
后果:如果需要控制表单提交逻辑,应该在 action 函数或 onSubmit 事件中处理, 而不是依赖 useFormStatus。
❌ 错误代码:
function SubmitButton() {
const { pending } = useFormStatus();
const handleClick = () => {
if (pending) {
// ❌ 这样无法阻止表单提交!
return;
}
};
return <button onClick={handleClick}>提交</button>;
}✅ 修正代码:
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending} // ✅ 使用 disabled 属性阻止提交
>
提交
</button>
);
}
// 或者在 action 函数中处理验证逻辑
async function handleSubmit(formData: FormData) {
const email = formData.get('email');
if (!email || !email.includes('@')) {
// ✅ 返回错误信息
return { success: false, error: '邮箱格式不正确' };
}
// 提交逻辑...
}心智模型纠正:useFormStatus 是"只读显示器", 不是"控制器"。它只能显示表单的状态,不能改变表单的行为。
陷阱 4: 在条件语句或循环中使用 useFormStatus
问题:违反 React Hooks 规则,Hooks 必须在组件顶层调用, 不能在条件语句、循环或嵌套函数中使用。
后果:React 无法正确追踪 Hook 的调用顺序,导致状态混乱或运行时报错。
❌ 错误代码:
function SubmitButton() {
// ❌ 在条件语句中使用 Hook
if (someCondition) {
const { pending } = useFormStatus();
return <button disabled={pending}>提交</button>;
}
return <button>提交</button>;
}✅ 修正代码:
function SubmitButton() {
// ✅ 在组件顶层调用 Hook
const { pending } = useFormStatus();
// 然后在条件语句中使用返回值
if (someCondition) {
return <button disabled={pending}>提交</button>;
}
return <button>提交</button>;
}最佳实践
✅ 推荐模式
1. 创建可复用的按钮组件
将 SubmitButton 组件单独抽离,利用 useFormStatus 自动获取状态, 无需通过 props 传递 pending。
function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending } = useFormStatus();
return (
<button disabled={pending} aria-busy={pending}>
{pending ? '提交中...' : children}
</button>
);
}
// 使用时无需传递任何 props
<form action={handleSubmit}>
<SubmitButton>提交</SubmitButton>
</form>2. 结合 useActionState 使用
useActionState 管理表单的完整状态(成功/失败/错误), useFormStatus 管理提交过程中的 UI 状态(pending/loading), 两者配合实现完美的表单体验。
// useActionState: 管理表单结果状态
const [state, formAction] = useActionState(submitForm, initialState);
// useFormStatus: 管理提交过程状态
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>提交</button>;
}3. 使用 pending 禁用所有交互元素
不仅禁用按钮,还应该禁用输入框、下拉框等其他交互元素, 防止用户在提交过程中修改数据。
function FormInput({ name, ...props }: InputProps) {
const { pending } = useFormStatus();
return (
<input
name={name}
disabled={pending} // 提交期间禁用
{...props}
/>
);
}
function Select({ name, children }: SelectProps) {
const { pending } = useFormStatus();
return (
<select name={name} disabled={pending}>
{children}
</select>
);
}4. 提供视觉反馈
除了禁用按钮,还应该显示加载动画、改变按钮颜色、添加 aria 属性等, 让用户清楚知道表单正在提交。
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
aria-busy={pending} // 辅助技术识别
className="flex items-center justify-center gap-2"
>
{pending && (
<LoadingSpinner className="animate-spin h-4 w-4" />
)}
{pending ? '提交中...' : '提交'}
</button>
);
}5. 在 TypeScript 中使用类型推断
useFormStatus 的返回值具有完整的类型定义,可以正确推断 pending、data 等属性的类型。
function StatusDisplay() {
const { pending, data, method, action } = useFormStatus();
// pending: boolean
// data: FormData | null
// method: 'GET' | 'POST' | undefined
// action: string | function | undefined
if (pending && data) {
// TypeScript 知道这里 data 不是 null
const email = data.get('email');
// email: string | File | null
}
return <div>{pending ? '提交中' : '空闲'}</div>;
}useFormStatus vs useActionState 对比
| 特性 | useFormStatus | useActionState |
|---|---|---|
| 用途 | 获取表单提交过程的状态 | 管理表单的完整生命周期状态 |
| 返回值 | { pending, data, method, action } | [state, formAction, isPending] |
| 使用位置 | 表单内的子组件 | 拥有表单的父组件 |
| 管理错误状态 | ❌ 不支持 | ✅ 支持(通过 state.error) |
| pending 含义 | 表单提交中 | action 函数执行中 |
| 典型用法 | 按钮、输入框组件 | 主表单组件 |
使用建议
- 优先使用 useFormStatus 处理子组件的 loading 状态,避免 props 穿透
- 在父组件使用 useActionState 管理表单的完整状态(成功/失败/错误)
- 结合两者使用,构建用户体验优秀的表单
- 记住 useFormStatus 只能读取状态,不能修改表单行为
- 确保 useFormStatus 在 <form> 内部使用,否则无法获取状态