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 和错误状态。
import { useActionState } from 'react';
import { useState } from 'react';
interface FormState {
success: boolean;
error: string | null;
pending: boolean;
}
function SubmitForm() {
const [state, formAction] = useActionState<FormState>(
async (prevState: FormState, formData: FormData) => {
// 模拟 API 调用
const name = formData.get('name') as string;
if (!name) {
return { success: false, error: '姓名不能为空', pending: false };
}
if (name.length < 2) {
return { success: false, error: '姓名至少需要2个字符', pending: false };
}
// 模拟异步提交
await new Promise(resolve => setTimeout(resolve, 1000));
return { success: true, error: null, pending: false };
},
{ success: false, error: null, pending: false }
);
return (
<form action={formAction} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">提交表单</h2>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">姓名</label>
<input
type="text"
name="name"
className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={state.pending}
/>
</div>
<button
type="submit"
disabled={state.pending}
className="w-full bg-blue-500 text-white py-2 rounded disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{state.pending ? '提交中...' : '提交'}
</button>
{state.error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 text-red-600 rounded">
{state.error}
</div>
)}
{state.success && (
<div className="mt-4 p-3 bg-green-50 border border-green-200 text-green-600 rounded">
提交成功!
</div>
)}
</form>
);
}效果: 表单自动管理提交状态。点击提交后,按钮自动禁用, 显示"提交中...",完成后显示成功或错误信息。无需手动管理 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 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 ${
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 ${
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 border border-red-200 text-red-600 rounded">
{state.error}
</div>
)}
{/* 成功消息 */}
{state.success && (
<div className="mt-4 p-3 bg-green-50 border border-green-200 text-green-600 rounded">
订阅成功!
</div>
)}
</form>
);
}效果: Server Action 在服务器端执行,处理表单验证、API 调用、数据库操作等。 客户端组件自动管理 pending 状态和错误显示,代码简洁且类型安全。
示例 3: 乐观更新(高级场景)
在提交期间立即更新 UI,提供即时反馈,失败时自动回滚。
import { useActionState } from 'react';
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
error: string | null;
pending: boolean;
}
// 添加待办事项的 action
async function addTodo(prevState: TodoState, formData: FormData) {
const text = formData.get('text') as string;
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000));
const newTodo: Todo = {
id: Date.now().toString(),
text,
completed: false,
};
return {
...prevState,
todos: [...prevState.todos, newTodo],
error: null,
};
}
function TodoList() {
const [state, formAction] = useActionState(addTodo, {
todos: [
{ id: '1', text: '学习 React 19', completed: false },
{ id: '2', text: '编写文档', completed: true },
],
error: null,
pending: false,
});
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">待办事项</h2>
{/* 添加待办表单 */}
<form action={formAction} className="mb-6">
<input
type="text"
name="text"
placeholder="添加新待办..."
className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={state.pending}
required
/>
<button
type="submit"
disabled={state.pending}
className="ml-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-300"
>
{state.pending ? '添加中...' : '添加'}
</button>
</form>
{/* 错误显示 */}
{state.error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-600 rounded">
{state.error}
</div>
)}
{/* 待办列表 */}
<ul className="space-y-2">
{state.todos.map(todo => (
<li
key={todo.id}
className="flex items-center gap-3 p-3 border rounded"
>
<input
type="checkbox"
checked={todo.completed}
readOnly
className="w-4 h-4"
/>
<span className={todo.completed ? 'line-through text-gray-400' : ''}>
{todo.text}
</span>
</li>
))}
</ul>
</div>
);
}效果: 用户点击添加后,虽然 API 调用需要1秒,但可以立即看到新待办出现在列表中。 如果提交失败,React 会自动回滚状态并显示错误。
示例 4: 多阶段表单(生产级)
展示如何处理复杂的表单流程,包括验证、确认和完成阶段。
import { useActionState } from 'react';
interface FormState {
stage: 'input' | 'confirm' | 'success' | 'error';
data: {
name: string;
email: string;
message: string;
} | null;
errors: Record<string, string>;
pending: boolean;
}
// 表单提交 action
async function submitContactForm(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const stage = prevState.stage;
// 第一阶段: 验证表单数据
if (stage === 'input') {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const message = formData.get('message') as string;
const errors: Record<string, string> = {};
if (!name || name.trim().length < 2) {
errors.name = '姓名至少需要2个字符';
}
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = '请输入有效的邮箱地址';
}
if (!message || message.trim().length < 10) {
errors.message = '留言至少需要10个字符';
}
if (Object.keys(errors).length > 0) {
return {
stage: 'input',
data: { name, email, message },
errors,
pending: false,
};
}
// 数据有效,进入确认阶段
return {
stage: 'confirm',
data: { name, email, message },
errors: {},
pending: false,
};
}
// 第二阶段: 提交到服务器
if (stage === 'confirm') {
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(prevState.data),
});
if (!response.ok) {
throw new Error('提交失败');
}
// 提交成功
return {
stage: 'success',
data: null,
errors: {},
pending: false,
};
} catch (error) {
return {
stage: 'error',
data: prevState.data,
errors: { _form: error instanceof Error ? error.message : '提交失败' },
pending: false,
};
}
}
// 其他阶段返回当前状态
return prevState;
}
function ContactForm() {
const [state, formAction] = useActionState(submitContactForm, {
stage: 'input',
data: null,
errors: {},
pending: false,
});
// 输入阶段
if (state.stage === 'input') {
return (
<form action={formAction} className="max-w-md mx-auto p-6 bg-white 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"
disabled={state.pending}
/>
{state.errors.name && (
<p className="mt-1 text-sm text-red-500">{state.errors.name}</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"
disabled={state.pending}
/>
{state.errors.email && (
<p className="mt-1 text-sm text-red-500">{state.errors.email}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="message" className="block text-sm font-medium mb-2">留言</label>
<textarea
id="message"
name="message"
rows={4}
className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={state.pending}
/>
{state.errors.message && (
<p className="mt-1 text-sm text-red-500">{state.errors.message}</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"
>
{state.pending ? '验证中...' : '下一步'}
</button>
</form>
);
}
// 确认阶段
if (state.stage === 'confirm' && state.data) {
return (
<form action={formAction} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">确认信息</h2>
<div className="space-y-3 mb-6">
<p><strong>姓名:</strong> {state.data.name}</p>
<p><strong>邮箱:</strong> {state.data.email}</p>
<p><strong>留言:</strong></p>
<p className="p-3 bg-gray-50 rounded">{state.data.message}</p>
</div>
<div className="flex gap-3">
<button
type="submit"
disabled={state.pending}
className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600 disabled:bg-gray-300"
>
{state.pending ? '提交中...' : '确认提交'}
</button>
{/* 返回按钮: 通过隐藏字段传递返回指令 */}
<button
type="submit"
formNoValidate
onClick={() => {
// 返回输入阶段
window.history.back();
}}
className="flex-1 bg-gray-200 text-gray-700 py-2 rounded hover:bg-gray-300"
>
返回修改
</button>
</div>
</form>
);
}
// 成功阶段
if (state.stage === 'success') {
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-green-100 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold mb-4 text-green-600">提交成功!</h2>
<p className="text-gray-600 mb-6">我们已收到您的留言,将尽快回复。</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
提交新的留言
</button>
</div>
);
}
// 错误阶段
if (state.stage === 'error') {
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow text-center">
<div className="w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="text-2xl font-bold mb-4 text-red-600">提交失败</h2>
<p className="text-gray-600 mb-6">{state.errors._form}</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
重新提交
</button>
</div>
);
}
return null;
}效果: 表单有清晰的流程:输入 → 确认 → 成功/错误。 每个阶段都由 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 表单 |