表单与数据变更
表单让您能够在网页应用中创建和更新数据。Next.js 通过 服务端操作 (Server Actions) 提供了强大的表单提交与数据变更处理方案。
示例
服务端操作原理
使用服务端操作时,您无需手动创建 API 端点,而是直接定义可在组件中调用的异步服务端函数。
🎥 观看: 通过应用路由学习表单与数据变更 → YouTube (10分钟)。
服务端操作可在服务端组件中定义,或从客户端组件调用。在服务端组件中定义操作可使表单在无 JavaScript 环境下正常工作,实现渐进增强。
在 next.config.js
中启用服务端操作:
module.exports = {
experimental: {
serverActions: true,
},
}
须知:
- 从服务端组件调用服务端操作的表单可在无 JavaScript 环境下运行。
- 从客户端组件调用服务端操作的表单会在 JavaScript 未加载时排队提交,优先保证客户端水合。
- 服务端操作继承所在页面或布局的 运行时环境。
- 服务端操作兼容完全静态的路由(包括使用 ISR 重新验证数据)。
重新验证缓存数据
服务端操作与 Next.js 缓存与重新验证 架构深度集成。表单提交时,服务端操作可更新缓存数据并重新验证应变更的缓存键。
不同于传统应用每个路由只能有一个表单,服务端操作支持每个路由多个操作。此外,表单提交时浏览器无需刷新。在单次网络往返中,Next.js 可同时返回更新后的 UI 和刷新数据。
查看下方 通过服务端操作重新验证数据 的示例。
示例
纯服务端表单
要创建纯服务端表单,需在服务端组件中定义服务端操作。操作可内联定义(在函数顶部添加 "use server"
指令),或在单独文件中定义(文件顶部添加指令)。
export default function Page() {
async function create(formData: FormData) {
'use server'
// 变更数据
// 重新验证缓存
}
return <form action={create}>...</form>
}
export default function Page() {
async function create(formData) {
'use server'
// 变更数据
// 重新验证缓存
}
return <form action={create}>...</form>
}
须知:
<form action={create}>
接收 FormData 数据类型。上例中通过 HTMLform
提交的 FormData 可在服务端操作create
中访问。
重新验证数据
服务端操作允许您按需使 Next.js 缓存 失效。您可以使用 revalidatePath
使整个路由段失效:
'use server'
import { revalidatePath } from 'next/cache'
export default async function submit() {
await submitForm()
revalidatePath('/')
}
'use server'
import { revalidatePath } from 'next/cache'
export default async function submit() {
await submitForm()
revalidatePath('/')
}
或使用 revalidateTag
通过缓存标签使特定数据获取失效:
'use server'
import { revalidateTag } from 'next/cache'
export default async function submit() {
await addPost()
revalidateTag('posts')
}
'use server'
import { revalidateTag } from 'next/cache'
export default async function submit() {
await addPost()
revalidateTag('posts')
}
重定向
若要在服务端操作完成后重定向用户,可使用 redirect
跳转到任意绝对或相对 URL:
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export default async function submit() {
const id = await addPost()
revalidateTag('posts') // 更新缓存文章
redirect(`/post/${id}`) // 导航到新路由
}
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export default async function submit() {
const id = await addPost()
revalidateTag('posts') // 更新缓存文章
redirect(`/post/${id}`) // 导航到新路由
}
表单验证
推荐使用 required
和 type="email"
等 HTML 验证进行基础表单验证。
如需高级服务端验证,可使用 zod 等模式验证库验证解析后的表单数据结构:
import { z } from 'zod'
const schema = z.object({
// ...
})
export default async function submit(formData: FormData) {
const parsed = schema.parse({
id: formData.get('id'),
})
// ...
}
import { z } from 'zod'
const schema = z.object({
// ...
})
export default async function submit(formData) {
const parsed = schema.parse({
id: formData.get('id'),
})
// ...
}
显示加载状态
使用 useFormStatus
钩子显示表单在服务端提交时的加载状态。该钩子只能作为使用服务端操作的 form
元素的子组件使用。
例如以下提交按钮:
<SubmitButton />
可在包含服务端操作的表单中使用:
import { SubmitButton } from '@/app/submit-button'
export default async function Home() {
return (
<form action={...}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
import { SubmitButton } from '@/app/submit-button'
export default async function Home() {
return (
<form action={...}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
错误处理
服务端操作 (Server Actions) 也可以返回可序列化对象。例如,您的服务端操作可以处理创建新项目时的错误:
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
await createItem(formData.get('todo'))
return revalidatePath('/')
} catch (e) {
return { message: 'Failed to create' }
}
}
'use server'
export async function createTodo(prevState, formData) {
try {
await createItem(formData.get('todo'))
return revalidatePath('/')
} catch (e) {
return { message: 'Failed to create' }
}
}
然后,在客户端组件 (Client Component) 中,您可以读取这个值并显示错误信息。
'use client'
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
const initialState = {
message: null,
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)
return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
'use client'
import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'
const initialState = {
message: null,
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" aria-disabled={pending}>
Add
</button>
)
}
export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState)
return (
<form action={formAction}>
<label htmlFor="todo">Enter Task</label>
<input type="text" id="todo" name="todo" required />
<SubmitButton />
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
</form>
)
}
乐观更新 (Optimistic Updates)
使用 useOptimistic
在服务端操作完成前乐观地更新 UI,而无需等待响应:
'use client'
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
messages,
(state: Message[], newMessage: string) => [
...state,
{ message: newMessage },
]
)
return (
<div>
{optimisticMessages.map((m, k) => (
<div key={k}>{m.message}</div>
))}
<form
action={async (formData: FormData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">发送</button>
</form>
</div>
)
}
'use client'
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
)
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form
action={async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">发送</button>
</form>
</div>
)
}
设置 Cookies
您可以在服务端操作中使用 cookies
函数设置 cookie:
'use server'
import { cookies } from 'next/headers'
export async function create() {
const cart = await createCart()
cookies().set('cartId', cart.id)
}
'use server'
import { cookies } from 'next/headers'
export async function create() {
const cart = await createCart()
cookies().set('cartId', cart.id)
}
读取 Cookies
您可以在服务端操作中使用 cookies
函数读取 cookie:
'use server'
import { cookies } from 'next/headers'
export async function read() {
const auth = cookies().get('authorization')?.value
// ...
}
'use server'
import { cookies } from 'next/headers'
export async function read() {
const auth = cookies().get('authorization')?.value
// ...
}
删除 Cookies
您可以在服务端操作中使用 cookies
函数删除 cookie:
'use server'
import { cookies } from 'next/headers'
export async function delete() {
cookies().delete('name')
// ...
}
'use server'
import { cookies } from 'next/headers'
export async function delete() {
cookies().delete('name')
// ...
}
查看更多示例了解如何从服务端操作中删除 cookie。