React Server Actions 在表单提交中的使用模式
React Server Actions 允许你在服务端直接运行函数,而无需手动创建 API 路由。这种方式极大地简化了表单提交流程。以下是几种核心使用模式的实操指南。
模式一:基础表单提交
这是最直接的模式,适用于简单的数据提交场景,无需复杂的客户端状态管理。
1. 定义 Server Action
在 app/actions.ts 文件中创建一个异步函数,并在文件顶部添加 'use server' 指令。
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createTodo(formData: FormData) {
const title = formData.get('title') as string
// **执行**数据库操作
await db.todo.create({
data: {
title,
},
})
// **刷新**缓存以更新 UI
revalidatePath('/')
}
2. 绑定 Action 到表单
在组件中导入刚才创建的函数,并将其传递给 <form> 元素的 action 属性。
// app/page.tsx
import { createTodo } from './actions'
export default function Page() {
return (
<form action={createTodo}>
<input
type="text"
name="title"
required
placeholder="输入待办事项..."
/>
<button type="submit">**提交**</button>
</form>
)
}
在此模式下,浏览器会自动将表单数据作为 FormData 对象发送给服务端函数,无需手动编写 fetch 请求。
模式二:提交后重定向
在用户提交表单成功后,通常需要将其跳转到其他页面。
1. 引入 redirect 函数
从 next/navigation 导入 redirect 函数。
2. 在逻辑末尾调用重定向
在 Server Action 的数据库操作完成后,调用 redirect 并指定目标路径。
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { db } from '@/lib/db'
export async function updateUser(formData: FormData) {
const id = formData.get('id') as string
const name = formData.get('name') as string
await db.user.update({
where: { id },
data: { name },
})
// **强制**重定向到用户详情页
redirect(`/users/${id}`)
}
注意:redirect 会抛出一个错误以中断执行,因此请确保将其放置在所有逻辑的最后。
模式三:错误处理与表单状态
当需要向用户反馈服务器返回的错误信息(如验证失败、重复提交等)时,需使用 useFormState Hook。
1. 修改 Action 返回状态
调整 Server Action,使其不再直接抛出错误,而是返回一个包含错误信息的对象。
// app/actions.ts
'use server'
export async function submitForm(
prevState: { message: string },
formData: FormData
) {
const email = formData.get('email') as string
if (!email.includes('@')) {
return { message: '请输入有效的邮箱地址' }
}
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000))
return { message: '提交成功!' }
}
2. 在客户端绑定 State
在客户端组件中(必须添加 'use client' 指令),使用 useFormState 包裹 Action。
// app/ContactForm.tsx
'use client'
import { useFormState } from 'react-dom'
import { submitForm } from './actions'
export default function ContactForm() {
const [state, formAction] = useFormState(submitForm, { message: '' })
return (
<form action={formAction}>
<input type="email" name="email" required />
<button type="submit">发送</button>
{/* **渲染**状态消息 */}
<p style={{ color: state.message.includes('成功') ? 'green' : 'red' }}>
{state.message}
</p>
</form>
)
}
3. 模式对比
下表总结了不同场景下的处理方式差异:
| 场景需求 | 推荐模式 | 关键 API/Hook | 返回值处理 |
|---|---|---|---|
| 简单数据写入 | 模式一 | action={fn} |
无需处理,自动刷新 |
| 提交后跳转 | 模式二 | redirect(url) |
抛出 NextResponse |
| 显示错误/成功消息 | 模式三 | useFormState |
返回 State 对象 |
模式四:提交中的待定状态
为了提升用户体验,需要在提交过程中禁用按钮并显示加载动画。这需要配合 useFormStatus Hook 使用。
1. 提取按钮为独立组件
将提交按钮拆分到一个独立的子组件中,并标记为 'use client'。
// app/SubmitButton.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button
type="submit"
disabled={pending}
aria-disabled={pending}
>
{pending ? '提交中...' : '提交'}
</button>
)
}
注意:useFormStatus 必须在 <form> 的内部使用,它才会自动关联父级表单的状态。
2. 在主表单中引用
在主表单组件中替换原有的 <button> 标签为新组件。
// app/page.tsx
'use client'
import { useFormState } from 'react-dom'
import { submitForm } from './actions'
import { SubmitButton } from './SubmitButton'
export default function FormWithPending() {
const [state, formAction] = useFormState(submitForm, null)
return (
<form action={formAction}>
<input type="text" name="username" />
<SubmitButton />
{state?.message && <p>{state.message}</p>}
</form>
)
}
模式五:处理复杂数据结构
默认的 FormData 只能处理字符串或文件。如果表单包含复杂对象或嵌套数据,需在客户端进行预处理,或将数据序列化为 JSON 字符串传递。
1. 客户端序列化
在 action 调用前(通常通过 action 属性直接处理时很难做到,更推荐使用调用方式),或者使用隐藏字段存储 JSON。
如果必须使用表单提交模式,可以在表单中添加隐藏字段:
// 假设 complexData 是一个对象
<input
type="hidden"
name="complexData"
value={JSON.stringify(complexData)}
/>
2. 服务端解析
在 Server Action 中获取该字段并解析 JSON。
// app/actions.ts
'use server'
export async function handleComplexData(formData: FormData) {
const rawData = formData.get('complexData') as string
// **解析** JSON 字符串
try {
const data = JSON.parse(rawData)
await db.process(data)
} catch (e) {
console.error('数据解析失败', e)
}
}
这种方式适用于需要配合浏览器默认表单行为,但又需传递结构化数据的场景。

暂无评论,快来抢沙发吧!