表單與資料異動

表單讓您能在網頁應用程式中建立與更新資料。Next.js 提供了使用 伺服器動作 (Server Actions) 來處理表單提交與資料異動的強大方式。

範例

伺服器動作如何運作

使用伺服器動作時,您無需手動建立 API 端點。相反地,您可以定義能直接從元件呼叫的非同步伺服器函式。

🎥 觀看: 透過應用程式路由了解更多關於表單與資料異動的內容 → YouTube (10 分鐘)

伺服器動作可在伺服器元件中定義或從客戶端元件呼叫。在伺服器元件中定義動作能讓表單在無需 JavaScript 的情況下運作,實現漸進增強。

next.config.js 檔案中啟用伺服器動作:

next.config.js
module.exports = {
  experimental: {
    serverActions: true,
  },
}

須知事項:

  • 從伺服器元件呼叫伺服器動作的表單可在無 JavaScript 的情況下運作。
  • 從客戶端元件呼叫伺服器動作的表單會在 JavaScript 尚未載入時排隊提交,優先處理客戶端水合。
  • 伺服器動作繼承使用它們的頁面或佈局的 執行環境 (runtime)
  • 伺服器動作可與完全靜態的路由搭配使用(包括使用 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}`) // 導航至新路由
}

表單驗證

我們建議使用 HTML 驗證如 requiredtype="email" 來進行基本表單驗證。

對於更進階的伺服器端驗證,可使用架構驗證函式庫如 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 鉤子來顯示表單在伺服器提交時的載入狀態。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">Send</button>
      </form>
    </div>
  )
}

您可以在伺服器動作中使用 cookies 函式設定 cookie:

'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}

您可以在伺服器動作中使用 cookies 函式讀取 cookie:

'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}

您可以在伺服器動作中使用 cookies 函式刪除 cookie:

'use server'

import { cookies } from 'next/headers'

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

查看其他範例了解如何從伺服器動作刪除 cookie。

On this page