useActionState
管理表单 Server Actions 的状态,简化表单错误处理和乐观 UI 更新
核心概述
痛点: 表单状态管理的复杂性
在 React 19 之前,管理表单状态(特别是与 Server Actions 集成时)需要大量样板代码:
- 手动管理 pending 状态: 需要单独的 state 来追踪表单是否正在提交
- 错误处理冗长: 需要手动 try-catch、管理 error state、显示错误信息
- 乐观更新困难: 要想在提交期间立即更新 UI,需要额外的状态管理逻辑
- 与 Server Actions 集成繁琐: 需要手动处理 formData、序列化、响应解析等
解决方案: 声明式表单状态管理
useActionState 是 React 19 引入的 Hook,专门用于简化基于 Server Actions 的表单状态管理。 它自动处理:
- 自动 pending 追踪: 自动管理表单提交的进行中状态
- 统一错误处理: 自动捕获并返回 action 函数抛出的错误或返回的错误对象
- 乐观 UI 更新: 支持在提交期间立即更新 UI,提供即时反馈
- FormData 自动序列化: 自动将表单数据转换为 FormData 传递给 action
适用场景
- ✅ Server Actions 表单: 与 Next.js App Router 的 Server Actions 完美集成
- ✅ 异步表单提交: 替代传统的 onSubmit + async/await 模式
- ✅ 需要加载状态的表单: 自动管理提交按钮的禁用状态
- ✅ 需要错误处理的表单: 统一的错误状态管理
💡 心智模型
将 useActionState 想象成"智能表单管家":
- • 自动追踪提交状态: 当用户点击提交时,管家自动标记"提交中", 禁用按钮,防止重复提交
- • 统一错误处理: 如果 action 返回错误,管家自动保存到 state.error, 你只需在 UI 中显示即可
- • 乐观更新支持: 你可以告诉管家"先假设成功,更新 UI", 如果后续失败,管家会自动回滚并显示错误
- • FormData 转换: 管家自动将表单字段打包成 FormData 传递给 action, 无需手动处理
关键: useActionState 专为 React 19 的 Server Actions 设计。 它将表单状态管理从"命令式"(手动管理每个状态)转变为"声明式"(定义 action,状态自动管理)。
技术规格
类型签名
function useActionState<State>(
action: (prevState: State, formData: FormData) => State | Promise<State>,
initialState: State,
permalink?: string
): [State, typeof action & { name?: string }]参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
action | (prevState, formData) => State | Promise<State> | 表单提交函数,接收前一个状态和 FormData,返回新状态(同步或异步) |
initialState | State | 初始状态,可以是对象、null 或 undefined |
permalink | string(可选) | 用于 URL 状态的固定链接(深度链接支持) |
返回值
返回一个包含两个值的数组:
- state: 当前状态,包含 action 返回的最新状态或错误
- formAction: 可传递给
<form action={formAction}>的函数, 包含原始 action 的所有属性和额外的 name 属性
运行机制
useActionState 基于 React 19 的 Server Actions 特性:
- Pending 状态: 当表单提交时,React 自动设置
state.pending = true, 提交完成后恢复 - 错误处理: 如果 action 抛出错误或返回包含 error 字段的对象,
state.error会被自动设置 - 乐观更新: 使用
permalink参数时, React 会根据 URL 状态自动管理表单的乐观更新和回滚 - FormData 转换: 表单提交时,React 自动收集表单数据并转换为 FormData
注意: useActionState 需要 React 19+。 它设计用于与 Server Actions 配合使用,但也可以用于客户端 action 函数。 返回的 formAction 必须传递给原生 <form> 元素的 action 属性, 不能用于普通按钮或其他事件处理程序。
实战演练
示例 1: 基础表单(客户端 Action)
最简单的用例:客户端异步表单提交,自动管理 pending 和错误状态。
效果: 表单自动管理提交状态。点击提交后,按钮自动禁用, 显示"提交中...",完成后显示成功或错误信息。无需手动管理 pending 状态。
示例 2: Server Actions(生产级,Next.js)
与 Next.js Server Actions 集成,实现真正的服务端表单处理。
// app/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
// ✅ 定义表单验证 schema
const formSchema = z.object({
email: z.string().email('无效的邮箱格式'),
name: z.string().min(2, '姓名至少需要2个字符'),
});
type FormState = {
success: boolean;
error: string | null;
fieldErrors?: Record<string, string[]>;
};
export async function submitForm(
prevState: FormState,
formData: FormData
): Promise<FormState> {
// 1. 验证表单数据
const validatedFields = formSchema.safeParse({
email: formData.get('email'),
name: formData.get('name'),
});
// 2. 返回验证错误
if (!validatedFields.success) {
return {
success: false,
error: '表单验证失败',
fieldErrors: validatedFields.error.flatten().fieldErrors,
};
}
const { email, name } = validatedFields.data;
try {
// 3. 调用 API 或数据库操作
const response = await fetch('https://api.example.com/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, name }),
});
if (!response.ok) {
throw new Error('订阅失败');
}
// 4. 重新验证相关页面缓存
revalidatePath('/');
return {
success: true,
error: null,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '未知错误',
};
}
}
// app/components/SubscribeForm.tsx
'use client';
import { useActionState } from 'react';
import { submitForm } from '@/app/actions';
function SubscribeForm() {
const [state, formAction] = useActionState(submitForm, {
success: false,
error: null,
});
return (
<form action={formAction} className="max-w-md mx-auto p-6 bg-white dark:bg-gray-800 rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">订阅通讯</h2>
{/* 姓名字段 */}
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium mb-2">
姓名
</label>
<input
type="text"
id="name"
name="name"
className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 ${
state.fieldErrors?.name ? 'border-red-500' : ''
}`}
disabled={state.pending}
/>
{state.fieldErrors?.name && (
<p className="mt-1 text-sm text-red-500">
{state.fieldErrors.name[0]}
</p>
)}
</div>
{/* 邮箱字段 */}
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium mb-2">
邮箱
</label>
<input
type="email"
id="email"
name="email"
className={`w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 ${
state.fieldErrors?.email ? 'border-red-500' : ''
}`}
disabled={state.pending}
/>
{state.fieldErrors?.email && (
<p className="mt-1 text-sm text-red-500">
{state.fieldErrors.email[0]}
</p>
)}
</div>
{/* 提交按钮 */}
<button
type="submit"
disabled={state.pending}
className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{state.pending ? '提交中...' : '订阅'}
</button>
{/* 全局错误 */}
{state.error && !state.fieldErrors && (
<div className="mt-4 p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 rounded">
{state.error}
</div>
)}
{/* 成功消息 */}
{state.success && (
<div className="mt-4 p-3 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 text-green-600 dark:text-green-400 rounded">
订阅成功!
</div>
)}
</form>
);
}效果: Server Action 在服务器端执行,处理表单验证、API 调用、数据库操作等。 客户端组件自动管理 pending 状态和错误显示,代码简洁且类型安全。
示例 3: 乐观更新(高级场景)
在提交期间立即更新 UI,提供即时反馈,失败时自动回滚。
效果: 用户点击添加后,虽然 API 调用需要1秒,但可以立即看到新待办出现在列表中。 如果提交失败,React 会自动回滚状态并显示错误。
示例 4: 多阶段表单(生产级)
展示如何处理复杂的表单流程,包括验证、确认和完成阶段。
效果: 表单有清晰的流程:输入 → 确认 → 成功/错误。 每个阶段都由 action 函数控制,useActionState 自动管理状态转换和 pending 状态。
避坑指南
❌ 陷阱 1: 忘记返回新状态
问题: action 函数必须返回新状态。如果不返回任何值, 状态不会更新,UI 也不会反映提交结果。
// ❌ 错误: 忘记返回状态
async function submitForm(prevState, formData) {
const name = formData.get('name');
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ name }),
});
// ❌ 没有返回值!状态不会更新!
}
// ✅ 正确: 返回新状态
async function submitForm(prevState, formData) {
const name = formData.get('name');
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ name }),
});
if (!response.ok) {
// ✅ 返回错误状态
return {
...prevState,
error: '提交失败',
};
}
// ✅ 返回成功状态
return {
...prevState,
success: true,
error: null,
};
}心智模型纠正: useActionState 依赖 action 函数的返回值来更新状态。 如果 action 不返回任何值,useActionState 就无法知道操作的结果。
❌ 陷阱 2: 在 action 函数中访问组件状态
问题: action 函数(特别是 Server Actions)在服务器端执行, 无法访问客户端的组件状态。所有数据必须通过 FormData 传递。
// ❌ 错误: 在 Server Action 中访问组件状态
'use server';
async function submitForm(prevState, formData) {
// ❌ 无法访问组件的 state 或 props!
const componentState = someComponentState;
await fetch('/api/submit', {
body: JSON.stringify({ data: componentState }),
});
return { success: true };
}
// ✅ 正确: 所有数据通过 FormData 传递
'use server';
async function submitForm(prevState, formData) {
// ✅ 从 FormData 读取所有需要的数据
const name = formData.get('name');
const email = formData.get('email');
const userId = formData.get('userId');
await fetch('/api/submit', {
body: JSON.stringify({ name, email, userId }),
});
return { success: true };
}
// ✅ 客户端组件:通过隐藏字段传递额外数据
function Form() {
const [userId] = useState('user-123');
const [, formAction] = useActionState(submitForm, {});
return (
<form action={formAction}>
{/* ✅ 通过隐藏字段传递 userId */}
<input type="hidden" name="userId" value={userId} />
<input type="text" name="name" />
<input type="email" name="email" />
<button type="submit">提交</button>
</form>
);
}心智模型纠正: Server Actions 是独立的服务器端函数, 无法像客户端函数那样访问闭包中的状态。 所有数据必须显式通过 FormData 或隐藏字段传递。
❌ 陷阱 3: 用 useState 管理表单状态
问题: 当使用 useActionState 时,不应该再用 useState 管理 pending 和错误状态。 这会导致状态冲突和重复代码。
// ❌ 错误: 手动管理状态(useActionState 已经自动管理了)
function Form() {
const [isPending, setIsPending] = useState(false); // ❌ 重复!
const [error, setError] = useState(null); // ❌ 重复!
const [, formAction] = useActionState(async (prevState, formData) => {
setIsPending(true); // ❌ 不需要!
try {
await submitToServer(formData);
setError(null);
} catch (err) {
setError(err);
} finally {
setIsPending(false); // ❌ 不需要!
}
return { success: true };
}, {});
return (
<form action={formAction}>
<button disabled={isPending}>提交</button> {/* ❌ 使用手动状态 */}
{error && <p>{error}</p>}
</form>
);
}
// ✅ 正确: 使用 useActionState 的 state
function Form() {
const [state, formAction] = useActionState(async (prevState, formData) => {
// ✅ 直接提交,useActionState 会自动管理 pending
const response = await submitToServer(formData);
if (!response.ok) {
// ✅ 返回错误状态
return {
...prevState,
error: '提交失败',
};
}
// ✅ 返回成功状态
return {
...prevState,
error: null,
success: true,
};
}, { error: null, success: false });
return (
<form action={formAction}>
{/* ✅ 使用 useActionState 的 state */}
<button disabled={state.pending}>提交</button>
{state.error && <p>{state.error}</p>}
{state.success && <p>提交成功!</p>}
</form>
);
}心智模型纠正: useActionState 的核心价值就是"自动状态管理"。 如果手动管理 pending 和 error,就失去了使用 useActionState 的意义。
❌ 陷阱 4: 将 formAction 用于 onClick 等事件处理
问题: formAction 必须传递给原生 <form action={formAction}>, 不能用于普通按钮的 onClick 或其他事件处理程序。
// ❌ 错误: formAction 不能用于 onClick
function Form() {
const [, formAction] = useActionState(submitForm, {});
return (
<form>
<input type="text" name="text" />
{/* ❌ 这不会触发 useActionState */}
<button onClick={formAction}>提交</button>
</form>
);
}
// ❌ 错误: formAction 不能用于其他事件
function Form() {
const [, formAction] = useActionState(submitForm, {});
return (
<form>
<input type="text" name="text" />
<button type="button" onClick={formAction}>提交</button>
</form>
);
}
// ✅ 正确: formAction 必须用于 form 的 action 属性
function Form() {
const [state, formAction] = useActionState(submitForm, {});
return (
{/* ✅ 正确: formAction 传递给 action 属性 */}
<form action={formAction}>
<input type="text" name="text" />
<button type="submit">提交</button>
{state.pending && <p>提交中...</p>}
</form>
);
}
// ✅ 如果需要用普通按钮,可以使用 action 链接
function FormWithButton() {
const [state, formAction] = useActionState(submitForm, {});
return (
<form action={formAction}>
<input type="text" name="text" />
{/* ✅ 使用 formAction 的 name 属性创建提交按钮 */}
<button type="submit" name="intent" value="submit">
提交
</button>
<button type="submit" name="intent" value="cancel">
取消
</button>
</form>
);
}心智模型纠正: formAction 是专门为原生表单提交机制设计的。 它利用浏览器的内置表单处理流程,不是通用的事件处理函数。
最佳实践
✅ 推荐模式
1. 使用 Zod 进行表单验证
在 Server Action 中使用 Zod 进行严格的表单验证,提供类型安全。
'use server';
import { z } from 'zod';
// ✅ 定义验证 schema
const schema = z.object({
email: z.string().email('无效邮箱'),
age: z.number().min(18, '必须年满18岁'),
});
type FormState = {
success: boolean;
error: string | null;
fieldErrors: Record<string, string[]> | null;
};
export async function submitForm(
prevState: FormState,
formData: FormData
): Promise<FormState> {
// ✅ 解析和验证
const result = schema.safeParse({
email: formData.get('email'),
age: formData.get('age'),
});
// ✅ 返回字段级错误
if (!result.success) {
return {
success: false,
error: '表单验证失败',
fieldErrors: result.error.flatten().fieldErrors,
};
}
// ✅ 使用验证后的数据(类型安全)
const { email, age } = result.data;
await saveToDatabase({ email, age });
return {
success: true,
error: null,
fieldErrors: null,
};
}2. 返回结构化的错误信息
不仅返回全局错误,还要返回字段级错误,帮助用户精确定位问题。
interface FormState {
success: boolean;
error: string | null; // 全局错误
fieldErrors: { // 字段级错误
email?: string[];
name?: string[];
} | null;
}
function Form() {
const [state, formAction] = useActionState(submitForm, {
success: false,
error: null,
fieldErrors: null,
});
return (
<form action={formAction}>
<input name="email" />
{state.fieldErrors?.email && (
<p>{state.fieldErrors.email[0]}</p>
)}
{state.error && !state.fieldErrors && (
<p>{state.error}</p>
)}
</form>
);
}3. 利用 prevState 实现乐观更新
在 action 开始时返回乐观状态,提供即时反馈,失败时返回错误状态。
interface TodoState {
todos: Todo[];
pendingId: string | null; // 乐观添加的待办 ID
}
async function addTodo(prevState: TodoState, formData: FormData) {
const text = formData.get('text') as string;
const optimisticId = `temp-${Date.now()}`;
// ✅ 返回乐观状态
return {
todos: [
...prevState.todos,
{ id: optimisticId, text, completed: false },
],
pendingId: optimisticId,
};
}
function TodoList() {
const [state, formAction] = useActionState(addTodo, {
todos: [],
pendingId: null,
});
return (
<form action={formAction}>
{state.todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
isOptimistic={todo.id === state.pendingId}
/>
)}
</form>
);
}4. 与 permalink 结合使用
使用 permalink 参数支持 URL 状态,实现可深度链接的表单状态。
function EditProfile({ userId }: { userId: string }) {
const [state, formAction] = useActionState(
updateProfile,
{ success: false, error: null },
`/edit/${userId}` // ✅ permalink
);
// ✅ 表单状态会与 URL 同步
return <form action={formAction}>...</form>;
}⚠️ 使用建议
- 优先用于 Server Actions: useActionState 设计用于与 Server Actions 配合使用。 客户端 action 也可以使用,但优势不明显
- 简单表单优先: 对于简单表单,useActionState 可能过度设计。 考虑是否真的需要自动状态管理
- 错误处理要完整: 确保处理所有可能的错误情况, 包括验证错误、网络错误、服务器错误
- 类型安全: 使用 TypeScript 定义 FormState 类型, 确保返回值类型正确
📊 useActionState vs 传统方式
对比使用 useActionState 和传统方式的差异:
| 特性 | 传统方式 | useActionState |
|---|---|---|
| Pending 管理 | 手动 useState + setIsPending | ✅ 自动管理(state.pending) |
| 错误处理 | try-catch + setState({ error }) | ✅ 自动捕获(action 返回值) |
| FormData 处理 | 手动 new FormData(form) | ✅ 自动传递 FormData |
| 适用场景 | 所有表单 | ✅ Server Actions 表单 |