伺服器動作與資料變更

伺服器動作 (Server Actions) 是在伺服器端執行的非同步函式,可用於伺服器與客戶端元件中,處理 Next.js 應用程式中的表單提交與資料變更。

🎥 觀看影片: 深入了解使用伺服器動作處理表單與資料變更 → YouTube (10 分鐘)

慣例

伺服器動作可透過 React 的 "use server" 指令定義。您可將指令置於 async 函式頂端標記該函式為伺服器動作,或置於獨立檔案頂端標記該檔案所有匯出為伺服器動作。

伺服器元件

伺服器元件可使用行內函式層級或模組層級的 "use server" 指令。若要行內定義伺服器動作,請在函式主體頂端加入 "use server"

// 伺服器元件
export default function Page() {
  // 伺服器動作
  async function create() {
    'use server'

    // ...
  }

  return (
    // ...
  )
}

客戶端元件

客戶端元件僅能匯入使用模組層級 "use server" 指令的動作。

要在客戶端元件呼叫伺服器動作,請建立新檔案並在頂端加入 "use server" 指令。檔案內所有函式將被標記為可重複用於客戶端與伺服器元件的伺服器動作:

'use server'

export async function create() {
  // ...
}

您也可將伺服器動作作為 prop 傳遞給客戶端元件:

<ClientComponent updateItem={updateItem} />
app/client-component.jsx
'use client'

export default function ClientComponent({ updateItem }) {
  return <form action={updateItem}>{/* ... */}</form>
}

行為

  • 伺服器動作可透過 <form> 元素action 屬性呼叫:
    • 伺服器元件預設支援漸進增強,意味即使 JavaScript 尚未載入或停用,表單仍可提交。
    • 在客戶端元件中,呼叫伺服器動作的表單會在 JavaScript 未載入時排隊提交,優先處理客戶端水合。
    • 水合完成後,瀏覽器不會在表單提交時重新整理。
  • 伺服器動作不限於 <form>,可從事件處理器、useEffect、第三方函式庫及其他表單元素如 <button> 呼叫。
  • 伺服器動作與 Next.js 快取與重新驗證架構整合。當動作呼叫時,Next.js 可在單次伺服器往返中同時回傳更新的 UI 與新資料。
  • 底層實作上,動作使用 POST 方法,且僅此 HTTP 方法可呼叫它們。
  • 伺服器動作的參數與回傳值必須可被 React 序列化。請參閱 React 文件了解可序列化參數與值清單。
  • 伺服器動作是函式,意味可在應用程式中任何地方重複使用。
  • 伺服器動作繼承使用它們的頁面或佈局的執行環境
  • 伺服器動作繼承使用它們的頁面或佈局的路由區段設定,包含如 maxDuration 等欄位。

範例

表單

React 擴充了 HTML <form> 元素,允許透過 action prop 呼叫伺服器動作。

在表單中呼叫時,動作會自動接收 FormData 物件。您無需使用 React useState 管理欄位,而是可使用原生 FormData 方法提取資料:

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

須知事項:

傳遞額外參數

您可使用 JavaScript bind 方法傳遞額外參數給伺服器動作。

'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.js
'use server'

export async function updateUser(userId, formData) {
  // ...
}

須知事項:

  • 替代方案是將參數作為隱藏輸入欄位傳遞(例如 <input type="hidden" name="userId" value={userId} />)。然而,該值會成為渲染 HTML 的一部分且不會被編碼。
  • .bind 在伺服器與客戶端元件中皆可使用,也支援漸進增強。

等待狀態

您可使用 React useFormStatus 鉤子在表單提交時顯示等待狀態。

  • useFormStatus 回傳特定 <form> 的狀態,因此必須定義為 <form> 元素的子項
  • useFormStatus 是 React 鉤子,因此必須在客戶端元件中使用。
'use client'

import { useFormStatus } from 'react-dom'

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

  return (
    <button type="submit" disabled={pending}>
      新增
    </button>
  )
}

<SubmitButton /> 可嵌套於任何表單中:

import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'

// 伺服器元件
export default async function Home() {
  return (
    <form action={createItem}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

伺服器端驗證與錯誤處理

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

對於進階伺服器端驗證,您可使用如 zod 的函式庫在變更資料前驗證表單欄位:

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

  // 變更資料
}

在伺服器端驗證欄位後,您可在動作中回傳可序列化物件,並使用 React useFormState 鉤子向使用者顯示訊息。

  • 透過將動作傳遞給 useFormState,動作的函式簽名會變更為接收新的 prevStateinitialState 參數作為第一個引數。
  • useFormState 是 React 鉤子,因此必須在客戶端元件中使用。
'use server'

export async function createUser(prevState: any, formData: FormData) {
  // ...
  return {
    message: '請輸入有效的電子郵件',
  }
}

接著,您可將動作傳遞給 useFormState 鉤子,並使用回傳的 state 顯示錯誤訊息。

'use client'

import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">電子郵件</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
      <button>註冊</button>
    </form>
  )
}

須知事項:

  • 在變更資料前,您應始終確保使用者也有權限執行該動作。參閱驗證與授權

樂觀更新

您可使用 React useOptimistic 鉤子在伺服器動作完成前樂觀更新 UI,而非等待回應:

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

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

嵌套元素

您可在 <form> 內嵌套的元素如 <button><input type="submit"><input type="image"> 中呼叫伺服器動作。這些元素接受 formAction prop 或事件處理器

這在需要於單一表單中呼叫多個伺服器動作時很有用。例如,除了發布按鈕外,您可建立特定 <button> 元素儲存文章草稿。參閱 React <form> 文件了解更多。

程式化表單提交

您可以使用 requestSubmit() 方法觸發表單提交。例如,當使用者按下 + Enter 時,您可以監聽 onKeyDown 事件:

'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> 祖先元素的提交,從而呼叫伺服器動作 (Server Action)。

非表單元素

雖然在 <form> 元素中使用伺服器動作很常見,但它們也可以從程式碼的其他部分呼叫,例如事件處理器和 useEffect

事件處理器

您可以從事件處理器(如 onClick)呼叫伺服器動作。例如,增加按讚計數:

'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        按讚
      </button>
    </>
  )
}

為了提升使用者體驗,我們建議使用其他 React API,如 useOptimisticuseTransition,在伺服器動作執行完成前更新 UI,或顯示待處理狀態。

您也可以為表單元素新增事件處理器,例如在 onChange 時儲存表單欄位:

app/ui/edit-post.tsx
'use client'

import { publishPost, saveDraft } from './actions'

export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">發布</button>
    </form>
  )
}

對於這種可能快速觸發多次事件的情況,我們建議使用 防抖 (debouncing) 來避免不必要的伺服器動作呼叫。

useEffect

您可以使用 React 的 useEffect 鉤子,在元件掛載或依賴項變化時呼叫伺服器動作。這對於依賴全域事件或需要自動觸發的變更非常有用。例如,onKeyDown 處理應用程式快捷鍵、無限滾動的交集觀察器鉤子,或在元件掛載時更新瀏覽計數:

'use client'

import { incrementViews } from './actions'
import { useState, useEffect } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)

  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }

    updateViews()
  }, [])

  return <p>總瀏覽次數: {views}</p>
}

請記得考慮 useEffect 的行為與注意事項

錯誤處理

當錯誤被拋出時,它會被客戶端最近的 error.js<Suspense> 邊界捕獲。我們建議使用 try/catch 來返回錯誤,以便由您的 UI 處理。

例如,您的伺服器動作可以透過返回訊息來處理建立新項目時的錯誤:

'use server'

export async function createTodo(prevState: any, formData: FormData) {
  try {
    // 變更資料
  } catch (e) {
    throw new Error('建立任務失敗')
  }
}

須知:

重新驗證資料

您可以在伺服器動作中使用 revalidatePath API 重新驗證 Next.js 快取

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}

或者使用 revalidateTag 透過快取標籤使特定資料擷取失效:

'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}

重新導向

如果您想在伺服器動作完成後將使用者重新導向到不同的路由,可以使用 redirect API。redirect 需要在 try/catch 區塊外呼叫:

'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // 更新快取的文章
  redirect(`/post/${id}`) // 導向到新文章頁面
}

Cookies

您可以在伺服器動作中使用 cookies API 來 getsetdelete cookies:

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  // 取得 cookie
  const value = cookies().get('name')?.value

  // 設定 cookie
  cookies().set('name', 'Delba')

  // 刪除 cookie
  cookies().delete('name')
}

請參閱 其他範例 瞭解如何從伺服器動作刪除 cookies。

安全性

驗證與授權

您應該將伺服器動作視為公開的 API 端點,並確保使用者有權執行該動作。例如:

app/actions.ts
'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('您必須登入才能執行此操作')
  }

  // ...
}

閉包與加密

在元件內定義伺服器動作會建立一個 閉包,該動作可以存取外部函式的範圍。例如,publish 動作可以存取 publishVersion 變數:

export default function Page() {
  const publishVersion = await getLatestVersion();

  async function publish(formData: FormData) {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('發布時版本已變更');
    }
    ...
  }

  return <button action={publish}>發布</button>;
}

閉包在您需要擷取資料的 快照(例如 publishVersion)以便在動作被呼叫時使用時非常有用。

然而,為了實現這一點,捕獲的變數會被發送到客戶端,並在動作被呼叫時返回伺服器。為了防止敏感資料暴露給客戶端,Next.js 會自動加密閉包變數。每次建構 Next.js 應用程式時,都會為每個動作生成一個新的私鑰。這意味著動作只能在特定建構中被呼叫。

須知: 我們不建議僅依賴加密來防止敏感值暴露給客戶端。相反,您應該使用 React taint APIs 主動防止特定資料被發送到客戶端。

覆寫加密金鑰(進階)

當您在多個伺服器上自行託管 Next.js 應用程式時,每個伺服器實例最終可能會有不同的加密金鑰,導致潛在的不一致。

為了解決這個問題,您可以使用 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 環境變數覆寫加密金鑰。指定此變數可確保您的加密金鑰在建構之間保持一致,並且所有伺服器實例使用相同的金鑰。

這是一個進階使用情境,其中跨多個部署的一致加密行為對您的應用程式至關重要。您應該考慮標準的安全實踐,例如金鑰輪換和簽署。

須知: 部署到 Vercel 的 Next.js 應用程式會自動處理此問題。

允許的來源(進階)

由於伺服器動作可以在 <form> 元素中呼叫,這使它們容易受到 CSRF 攻擊

在底層,伺服器動作使用 POST 方法,並且只允許此 HTTP 方法呼叫它們。這在現代瀏覽器中防止了大多數 CSRF 漏洞,特別是預設情況下使用 SameSite cookies

作為額外的保護措施,Next.js 中的伺服器動作還會比較 Origin 標頭Host 標頭(或 X-Forwarded-Host)。如果這些不匹配,請求將被中止。換句話說,伺服器動作只能在託管它的頁面相同的主機上呼叫。

對於使用反向代理或多層後端架構(其中伺服器 API 與生產網域不同)的大型應用程式,建議使用配置選項 serverActions.allowedOrigins 來指定安全來源清單。該選項接受一個字串陣列。

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

瞭解更多關於 安全性與伺服器動作 的資訊。

其他資源

有關伺服器動作的更多資訊,請查閱以下 React 文件: