React Server Actions 是在服务器上执行的 Server Functions,可以从服务器组件和客户端组件调用,用于处理表单提交。本文介绍在 Next.js 中使用它们创建表单。关于单次往返响应、顺序分发、安全和部署等表单以外的行为,见 Server Actions 与数据修改。
警告:每个 Server Action 内都必须验证身份与授权,即使表单只显示在需要登录的页面中。详情见 数据安全指南。
工作原理
React 扩展了 HTML 的 form 元素,允许通过 action 属性调用 Server Action。用于表单时,函数自动接收 FormData,可用原生方法提取数据。
TypeScript 示例:
import { auth } from '@/lib/auth'
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const session = await auth()
if (!session?.user) {
throw new Error('Unauthorized')
}
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutate data
// revalidate the cache
}
return <form action={createInvoice}>...</form>
}
JavaScript 示例:
import { auth } from '@/lib/auth'
export default function Page() {
async function createInvoice(formData) {
'use server'
const session = await auth()
if (!session?.user) {
throw new Error('Unauthorized')
}
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutate data
// revalidate the cache
}
return <form action={createInvoice}>...</form>
}
提示:多个字段可以用 Object.fromEntries(formData) 转成对象,但结果还会包含以 $ACTION_ 开头的额外属性。
传递额外参数
除表单字段外,可以通过 JavaScript 的 bind 传递额外参数。例如,把 userId 传给 updateUser。
TypeScript:
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}
JavaScript:
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}
服务器函数会将 userId 作为额外参数接收。TypeScript:
'use server'
export async function updateUser(userId: string, formData: FormData) {}
JavaScript:
'use server'
export async function updateUser(userId, formData) {}
提示:也可使用隐藏字段,例如 <input type="hidden" name="userId" value={userId} />,但值会出现在渲染后的 HTML 中,不会被编码。bind 同时适用于服务器和客户端组件,也支持渐进增强。
表单验证
可以在客户端或服务器验证。客户端基础验证可用 required、type=”email” 等 HTML 属性。服务器端可用 Zod 或 Valibot 等 schema 库。
TypeScript:
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}
JavaScript:
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutate data
}
验证错误
要显示错误或消息,将定义 form 的组件改为客户端组件,使用 React 的 useActionState。
使用该 hook 后,服务器函数签名会变化,第一个参数变为新的 prevState 或 initialState。
TypeScript:
'use server'
import { z } from 'zod'
export async function createUser(initialState: any, formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
JavaScript:
'use server'
import { z } from 'zod'
// ...
export async function createUser(initialState, formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}
然后根据 state 对象有条件地显示错误消息。
TypeScript:
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>Sign up</button>
</form>
)
}
JavaScript:
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>Sign up</button>
</form>
)
}
等待状态
useActionState 暴露布尔值 pending,在操作执行期间可显示加载状态或禁用提交按钮。
TypeScript:
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* Other form elements */}
<button disabled={pending}>Sign up</button>
</form>
)
}
JavaScript:
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState)
return (
<form action={formAction}>
{/* Other form elements */}
<button disabled={pending}>Sign up</button>
</form>
)
}
也可以使用 useFormStatus,但需要单独创建显示等待状态的组件。例如,在等待期间禁用按钮。
TypeScript:
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
JavaScript:
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
随后将 SubmitButton 嵌套在表单中。
TypeScript:
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Other form elements */}
<SubmitButton />
</form>
)
}
JavaScript:
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
{/* Other form elements */}
<SubmitButton />
</form>
)
}
提示:React 19 的 useFormStatus 返回对象还包含 data、method、action 等键。早期版本只提供 pending。
提示:启用实验性 useOffline 配置后,Server Action 如果因断网中断,会保持等待并在网络恢复后完成,避免用户丢失提交。
乐观更新
通过 useOptimistic,可以在服务器函数完成之前先乐观更新界面,无需等待响应。
TypeScript:
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...state, { message: newMessage }])
const formAction = async (formData: FormData) => {
const message = formData.get('message') as string
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m, i) => (
<div key={i}>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
JavaScript:
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
)
const formAction = async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}
嵌套表单元素
form 内部的 button、input type=”submit” 和 input type=”image” 等元素,也可以调用 Server Actions。这些元素支持 formAction 属性或事件处理器。
需要一个表单执行多个操作时很有用。例如,除发布文章外,再添加专门保存草稿的按钮。更多信息见 React form 文档中的多种提交类型说明。
编程式提交
通过 requestSubmit() 可以主动提交表单。例如,监听 onKeyDown,让用户按 ⌘ + Enter 提交。
TypeScript:
'use client'
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
JavaScript:
'use client'
export function Entry() {
const handleKeyDown = (e) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
它会提交最近的 form 祖先元素,进而调用服务器函数。
原文:How to create forms with Server Actions。作者/维护者:Vercel 与 Next.js 文档贡献者。本文为原文的中文译文;代码保留原文内容。











暂无评论内容