React 19引入了useActionState Hook,将表单提交从手动管理loading/error状态的模式中解放出来。传统React表单处理需要维护多个state变量(isPending、error、data),在submit handler中手动控制异步流程。useActionState将这些逻辑收敛到一个Action函数中,配合form组件的action属性,表单提交的状态机由React框架自动管理。
useActionState基本用法
useActionState接收一个Action函数和初始状态,返回当前状态、包裹后的Action函数和isPending标志。Action函数签名为(previousState, formData) -> newState,在表单提交时被调用。
import { useActionState } from 'react';
function LoginForm() {
async function loginAction(prevState, formData) {
const email = formData.get('email');
const password = formData.get('password');
if (!email || !password) {
return { error: '邮箱和密码不能为空' };
}
if (!email.includes('@')) {
return { error: '邮箱格式不正确' };
}
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const data = await res.json();
return { error: data.message || '登录失败' };
}
return { success: true, redirect: '/dashboard' };
}
const [state, formAction, isPending] = useActionState(loginAction, null);
return (
<form action={formAction}>
<input type="email" name="email" required />
<input type="password" name="password" required />
<button type="submit" disabled={isPending}>
{isPending ? '登录中...' : '登录'}
</button>
{state?.error && <p className="error">{state.error}</p>}
</form>
);
}
关键变化:不再需要useState管理error和isPending,不再需要onSubmit + preventDefault样板代码。表单提交的触发、Action函数调用、状态更新全部由React协调。
useOptimistic乐观更新集成
useOptimistic Hook提供乐观更新能力,在提交开始时立即更新UI,提交失败时自动回滚:
import { useActionState, useOptimistic } from 'react';
function CommentList({ comments }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment) => [
...state,
{ id: 'temp-' + Date.now(), text: newComment, pending: true }
]
);
async function addCommentAction(prevState, formData) {
const text = formData.get('comment');
addOptimisticComment(text);
const res = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
if (!res.ok) {
return { error: '评论提交失败' };
}
return { success: true };
}
const [state, formAction, isPending] = useActionState(addCommentAction, null);
return (
<div>
<ul>
{optimisticComments.map(c => (
<li key={c.id} className={c.pending ? 'pending' : ''}>{c.text}</li>
))}
</ul>
<form action={formAction}>
<input type="text" name="comment" required />
<button type="submit" disabled={isPending}>发表评论</button>
</form>
</div>
);
}
Server Action与Next.js 15集成
React 19的Server Action允许直接在客户端组件中导入服务端函数。配合Next.js 15的App Router,表单提交可以绕过API路由层,直接在Server Action中操作数据库。
// app/login/actions.ts
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { compare } from 'bcryptjs'
import { z } from 'zod'
import { db } from '@/lib/db'
const loginSchema = z.object({
email: z.string().email('邮箱格式不正确'),
password: z.string().min(8, '密码至少8位'),
})
export async function loginAction(prevState, formData) {
const result = loginSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
if (!result.success) {
return { error: result.error.issues[0].message }
}
const { email, password } = result.data
const user = await db.user.findUnique({ where: { email } })
if (!user || !await compare(password, user.passwordHash)) {
return { error: '邮箱或密码错误' }
}
const session = await createSession(user.id)
cookies().set('session', session.token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
})
redirect('/dashboard')
}
// app/login/page.tsx
import { useActionState } from 'react'
import { loginAction } from './actions'
export default function LoginPage() {
const [state, formAction, isPending] = useActionState(loginAction, null)
return (
<form action={formAction}>
<input type="email" name="email" />
<input type="password" name="password" />
<button disabled={isPending}>
{isPending ? '登录中' : '登录'}
</button>
{state?.error && <p>{state.error}</p>}
</form>
)
}
Server Action的执行流程:表单提交触发POST请求到当前页面的RSC endpoint,Next.js在服务端执行loginAction函数,返回RSC payload更新客户端。客户端不接触密码或数据库连接,所有敏感操作在服务端完成。
表单校验策略
useActionState不排斥HTML原生表单校验。required、pattern、minlength等属性在表单提交前由浏览器执行首次校验,通过后才触发Action函数。配合Zod可实现在客户端快速预校验、服务端执行权威校验的双重保障。
const schema = z.object({
username: z.string().min(3, '用户名至少3个字符').max(20),
email: z.string().email('邮箱格式不正确'),
age: z.number().min(18, '必须年满18岁').max(120),
});
async function registerAction(prevState, formData) {
const input = {
username: formData.get('username'),
email: formData.get('email'),
age: Number(formData.get('age')),
};
const result = schema.safeParse(input);
if (!result.success) {
return { errors: result.error.flatten().fieldErrors };
}
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result.data),
});
if (res.status === 409) {
return { errors: { email: ['该邮箱已注册'] } };
}
return { success: true };
}
从React 18迁移到useActionState
迁移要点:
- 将onSubmit handler改造为(prevState, formData) => newState的Action函数,移除preventDefault
- 移除useState管理的error/isPending状态,改由useActionState返回值提供
- 将form onSubmit改为form action,值为useActionState返回的formAction
- 将手动读取e.target.elements.xxx.value改为formData.get(‘xxx’)
迁移前(React 18模式):
function OldForm() {
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsPending(true);
setError(null);
const formData = new FormData(e.target);
const res = await fetch('/api/submit', { method: 'POST', body: formData });
setIsPending(false);
if (!res.ok) setError('提交失败');
}
return <form onSubmit={handleSubmit}>...</form>;
}
迁移后(React 19模式):
function NewForm() {
async function action(prevState, formData) {
const res = await fetch('/api/submit', { method: 'POST', body: formData });
if (!res.ok) return { error: '提交失败' };
return { success: true };
}
const [state, formAction, isPending] = useActionState(action, null);
return <form action={formAction}>...</form>;
}
代码行数减少约40%,state变量从3个降为0个。isPending由React管理,消除了手动管理异步状态导致的竞态条件风险。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/react19useactionstate-biao-dan-zhuang-tai-guan-li-yu-biao/