跳到主要内容

如何使用服务端操作创建表单

React 服务端操作是在服务器上执行的服务端函数。它们可以在服务端和客户端组件中调用来处理表单提交。本指南将介绍如何在 Next.js 中使用服务端操作创建表单。

工作原理

React 扩展了 HTML <form> 元素,允许使用 action 属性调用服务端操作。

当在表单中使用时,函数会自动接收 FormData 对象。然后你可以使用原生的 FormData 方法提取数据:

app/invoices/page.tsx
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'

const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}

// 修改数据
// 重新验证缓存
}

return <form action={createInvoice}>...</form>
}

温馨提示: 在处理具有多个字段的表单时,你可以使用 entries() 方法与 JavaScript 的 Object.fromEntries()。例如:const rawFormData = Object.fromEntries(formData)

传递额外参数

除了表单字段外,你可以使用 JavaScript bind 方法向服务端函数传递额外参数。例如,要向 updateUser 服务端函数传递 userId 参数:

app/client-component.tsx
'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">更新用户名</button>
</form>
)
}

服务端函数将接收 userId 作为额外参数:

app/actions.ts
'use server'

export async function updateUser(userId: string, formData: FormData) {}

温馨提示:

  • 另一种方法是在表单中将参数作为隐藏输入字段传递(例如 <input type="hidden" name="userId" value={userId} />)。但是,该值将成为渲染 HTML 的一部分,不会被编码。
  • bind 在服务端和客户端组件中都有效,并支持渐进式增强。

表单验证

表单可以在客户端或服务器上进行验证。

  • 对于客户端验证,你可以使用 HTML 属性如 requiredtype="email" 进行基本验证。
  • 对于服务端验证,你可以使用像 zod 这样的库来验证表单字段。例如:
app/actions.ts
'use server'

import { z } from 'zod'

const schema = z.object({
email: z.string({
invalid_type_error: '无效邮箱',
}),
})

export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})

// 如果表单数据无效,提前返回
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}

// 修改数据
}

验证错误

要显示验证错误或消息,将定义 <form> 的组件转换为客户端组件,并使用 React useActionState

使用 useActionState 时,服务端函数签名将更改为接收新的 prevStateinitialState 参数作为其第一个参数。

app/actions.ts
'use server'

import { z } from 'zod'

export async function createUser(initialState: any, formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// ...
}

然后你可以根据 state 对象有条件地渲染错误消息。

app/ui/signup.tsx
'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">邮箱</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite">{state?.message}</p>
<button disabled={pending}>注册</button>
</form>
)
}

待处理状态

useActionState hook 暴露了一个 pending 布尔值,可用于在操作执行时显示加载指示器或禁用提交按钮。

app/ui/signup.tsx
'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}>
{/* 其他表单元素 */}
<button disabled={pending}>注册</button>
</form>
)
}

或者,你可以使用 useFormStatus hook 在操作执行时显示加载指示器。使用此 hook 时,你需要创建一个单独的组件来渲染加载指示器。例如,在操作待处理时禁用按钮:

app/ui/button.tsx
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
const { pending } = useFormStatus()

return (
<button disabled={pending} type="submit">
注册
</button>
)
}

然后你可以在表单内嵌套 SubmitButton 组件:

app/ui/signup.tsx
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'

export function Signup() {
return (
<form action={createUser}>
{/* 其他表单元素 */}
<SubmitButton />
</form>
)
}

温馨提示: 在 React 19 中,useFormStatus 在返回的对象上包含额外的键,如 data、method 和 action。如果你不使用 React 19,只有 pending 键可用。

乐观更新

你可以使用 React useOptimistic hook 在服务端函数完成执行之前乐观地更新 UI,而不是等待响应:

app/page.tsx
'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">发送</button>
</form>
</div>
)
}

嵌套表单元素

你可以在 <form> 内嵌套的元素中调用服务端操作,如 <button><input type="submit"><input type="image">。这些元素接受 formAction prop 或事件处理器。

这在你想在表单内调用多个服务端操作的情况下很有用。例如,除了发布帖子外,你还可以为保存帖子草稿创建一个特定的 <button> 元素。有关更多信息,请参阅 React <form> 文档

程序化表单提交

你可以使用 requestSubmit() 方法程序化地触发表单提交。例如,当用户使用 + Enter 键盘快捷键提交表单时,你可以监听 onKeyDown 事件:

app/entry.tsx
'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>
)
}

这将触发最近的 <form> 祖先的提交,这将调用服务端函数。