表单与数据变更

表单让您能够在网页应用中创建和更新数据。Next.js 通过 服务端操作 (Server Actions) 提供了强大的表单提交与数据变更处理方案。

示例

服务端操作原理

使用服务端操作时,您无需手动创建 API 端点,而是直接定义可在组件中调用的异步服务端函数。

🎥 观看: 通过应用路由学习表单与数据变更 → YouTube (10分钟)

服务端操作可在服务端组件中定义,或从客户端组件调用。在服务端组件中定义操作可使表单在无 JavaScript 环境下正常工作,实现渐进增强。

next.config.js 中启用服务端操作:

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>
}

须知<form action={create}> 接收 FormData 数据类型。上例中通过 HTML form 提交的 FormData 可在服务端操作 create 中访问。

重新验证数据

服务端操作允许您按需使 Next.js 缓存 失效。您可以使用 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')
}

重定向

若要在服务端操作完成后重定向用户,可使用 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}`) // 导航到新路由
}

表单验证

推荐使用 requiredtype="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'),
  })
  // ...
}

显示加载状态

使用 useFormStatus 钩子显示表单在服务端提交时的加载状态。该钩子只能作为使用服务端操作的 form 元素的子组件使用。

例如以下提交按钮:

'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

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

  return (
    <button type="submit" aria-disabled={pending}>
      添加
    </button>
  )
}

<SubmitButton /> 可在包含服务端操作的表单中使用:

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' }
  }
}

然后,在客户端组件 (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>
  )
}

乐观更新 (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>
  )
}

设置 Cookies

您可以在服务端操作中使用 cookies 函数设置 cookie:

'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
  // ...
}

删除 Cookies

您可以在服务端操作中使用 cookies 函数删除 cookie:

'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}

查看更多示例了解如何从服务端操作中删除 cookie。

On this page