跳至內容
建置您的應用程式資料提取伺服器動作與變更

伺服器動作與變更

伺服器動作非同步函式,在伺服器上執行。它們可以在伺服器和客戶端元件中被呼叫,以處理 Next.js 應用程式中的表單提交和資料變更。

🎥 影片:深入了解伺服器動作的變更 → YouTube (10 分鐘)

慣例

可以使用 React 的 "use server" 指令來定義伺服器動作。您可以將該指令放在 async 函式的頂部,將該函式標記為伺服器動作,或者放在單獨檔案的頂部,將該檔案的所有匯出標記為伺服器動作。

伺服器元件

伺服器元件可以使用行內函式層級或模組層級的 "use server" 指令。要以行內方式嵌入伺服器動作,請將 "use server" 新增到函式主體的頂部。

app/page.tsx
export default function Page() {
  // Server Action
  async function create() {
    'use server'
    // Mutate data
  }
 
  return '...'
}

客戶端元件

要在客戶端元件中呼叫伺服器動作,請建立一個新檔案,並在檔案頂部新增 "use server" 指令。檔案內所有匯出的函式都將被標記為伺服器動作,可在客戶端和伺服器元件中重複使用。

app/actions.ts
'use server'
 
export async function create() {}
app/ui/button.tsx
'use client'
 
import { create } from '@/app/actions'
 
export function Button() {
  return <button onClick={() => create()}>Create</button>
}

將動作作為 props 傳遞

您也可以將伺服器動作 (Server Action) 作為 prop 傳遞給客戶端元件 (Client Component)。

<ClientComponent updateItemAction={updateItem} />
app/client-component.tsx
'use client'
 
export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}

通常,Next.js TypeScript 插件會在 client-component.tsx 中標記 updateItemAction,因為它是一個函式,通常無法在客戶端-伺服器之間序列化。然而,名稱為 action 或以 Action 結尾的 props 會被假設為接收伺服器動作。這只是一種啟發式方法,因為 TypeScript 插件實際上並不知道它是接收伺服器動作還是普通函式。執行階段類型檢查仍會確保您不會意外地將函式傳遞給客戶端元件。

行為 快取和重新驗證架構整合。調用動作時,Next.js 可以在單次伺服器往返中返回更新的 UI 和新的數據。
  • 在幕後,動作使用 POST 方法,而且只有這個 HTTP 方法可以調用它們。
  • 伺服器動作的參數和返回值必須可由 React 序列化。有關可序列化參數和值的清單,請參閱 React 文件。
  • 伺服器動作是函式。這表示它們可以在應用程式的任何地方重複使用。
  • 伺服器動作會從使用它們的頁面或佈局繼承執行階段
  • 伺服器動作會從使用它們的頁面或佈局繼承路由區段設定,包括 maxDuration 等欄位。
  • 範例

    表單

    React 擴展了 HTML 的 <form> 元素,允許使用 action 屬性來調用伺服器動作。

    在表單中調用時,動作會自動接收 FormData 物件。您不需要使用 React 的 useState 來管理欄位,而是可以使用原生 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'),
        }
     
        // mutate data
        // revalidate cache
      }
     
      return <form action={createInvoice}>...</form>
    }

    注意事項

    傳遞額外參數

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

    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">Update User Name</button>
        </form>
      )
    }

    除了表單資料外,伺服器動作還會收到 `userId` 參數。

    app/actions.js
    'use server'
     
    export async function updateUser(userId, formData) {}

    注意事項:

    • 另一種方法是將參數作為隱藏的輸入欄位傳遞到表單中(例如 `<input type="hidden" name="userId" value={userId} />`)。 然而,該值將成為渲染後的 HTML 的一部分,並且不會被編碼。
    • `.bind` 適用於伺服器元件和客戶端元件。 它也支援漸進式增強。

    巢狀表單元素

    您也可以在巢狀於 `<form>` 內的元素(例如 `<button>`、`<input type="submit">` 和 `<input type="image">`)中呼叫伺服器動作。 這些元素接受 `formAction` 屬性或事件處理程式

    這在您想要在表單內呼叫多個伺服器動作的情況下非常有用。 例如,除了發佈文章之外,您還可以建立一個特定的 `<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>` 父元素的提交,進而呼叫伺服器動作。

    伺服器端表單驗證

    若要執行更進階的伺服器端驗證,您可以使用像是 zod 這樣的函式庫,在變更資料前先驗證表單欄位。

    app/actions.ts
    'use server'
     
    import { z } from 'zod'
     
    const schema = z.object({
      email: z.string({
        invalid_type_error: 'Invalid Email',
      }),
    })
     
    export default async function createUser(formData: FormData) {
      const validatedFields = schema.safeParse({
        email: formData.get('email'),
      })
     
      // Return early if the form data is invalid
      if (!validatedFields.success) {
        return {
          errors: validatedFields.error.flatten().fieldErrors,
        }
      }
     
      // Mutate data
    }

    在伺服器上驗證欄位後,您可以在您的 action 中返回一個可序列化物件,並使用 React 的 useFormState hook 向使用者顯示訊息。

    • 將 action 傳遞給 useFormState 後,action 的函式簽名會改變,將接收一個新的 prevStateinitialState 參數作為其第一個引數。
    • useFormState 是一個 React hook,因此必須在 Client Component(客戶端元件)中使用。
    app/actions.ts
    'use server'
     
    import { redirect } from 'next/navigation'
     
    export async function createUser(prevState: any, formData: FormData) {
      const res = await fetch('https://...')
      const json = await res.json()
     
      if (!res.ok) {
        return { message: 'Please enter a valid email' }
      }
     
      redirect('/dashboard')
    }

    接著,您可以將您的 action 傳遞給 useFormState hook,並使用返回的 state 顯示錯誤訊息。

    app/ui/signup.tsx
    'use client'
     
    import { useFormState } from 'react'
    import { createUser } from '@/app/actions'
     
    const initialState = {
      message: '',
    }
     
    export function Signup() {
      const [state, formAction] = useFormState(createUser, initialState)
     
      return (
        <form action={formAction}>
          <label htmlFor="email">Email</label>
          <input type="text" id="email" name="email" required />
          {/* ... */}
          <p aria-live="polite">{state?.message}</p>
          <button>Sign up</button>
        </form>
      )
    }

    注意事項

    • 這些範例使用 React 的 useFormState hook,它與 Next.js App Router 捆綁在一起。如果您使用的是 React 19,請改用 useActionState。詳情請參閱 React 文件

    處理中狀態

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

    useFormStatus hook 公開了一個 pending 布林值,可用於在 action 執行時顯示載入指示器。

    app/submit-button.tsx
    'use client'
     
    import { useFormStatus } from 'react-dom'
     
    export function SubmitButton() {
      const { pending } = useFormStatus()
     
      return (
        <button disabled={pending} type="submit">
          Sign Up
        </button>
      )
    }

    注意事項

    • 在 React 19 中,useFormStatus 在返回的物件中包含額外的鍵值,例如 data、method 和 action。如果您沒有使用 React 19,則只有 pending 鍵值可用。
    • 在 React 19 中,useActionState 在返回的狀態中也包含一個 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) => {
        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">Send</button>
          </form>
        </div>
      )
    }

    事件處理器

    雖然伺服器動作通常用於 <form> 元素內,但它們也可以透過事件處理器(例如 onClick)來呼叫。例如,要增加按讚數:

    app/like-button.tsx
    '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)
            }}
          >
            Like
          </button>
        </>
      )
    }

    您也可以將事件處理器添加到表單元素中,例如,在 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">Publish</button>
        </form>
      )
    }

    對於這種可能在短時間內觸發多個事件的情況,我們建議使用去抖動 (debouncing) 來防止不必要的伺服器動作呼叫。

    useEffect useEffect hook,在元件掛載或依賴項變更時呼叫伺服器動作。這對於依賴於全域事件或需要自動觸發的變更操作很有用。例如,應用程式快捷鍵的 onKeyDown、無限捲動的交叉觀察器 hook,或是在元件掛載時更新瀏覽次數。

    app/view-count.tsx
    '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>Total Views: {views}</p>
    }

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

    錯誤處理

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

    例如,您的伺服器動作可能會透過回傳訊息來處理建立新項目的錯誤。

    app/actions.ts
    'use server'
     
    export async function createTodo(prevState: any, formData: FormData) {
      try {
        // Mutate data
      } catch (e) {
        throw new Error('Failed to create task')
      }
    }

    注意事項

    重新驗證資料

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

    app/actions.ts
    'use server'
     
    import { revalidatePath } from 'next/cache'
     
    export async function createPost() {
      try {
        // ...
      } catch (error) {
        // ...
      }
     
      revalidatePath('/posts')
    }

    或使用 revalidateTag 使帶有快取標籤的特定資料提取失效

    app/actions.ts
    'use server'
     
    import { revalidateTag } from 'next/cache'
     
    export async function createPost() {
      try {
        // ...
      } catch (error) {
        // ...
      }
     
      revalidateTag('posts')
    }

    重新導向

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

    app/actions.ts
    'use server'
     
    import { redirect } from 'next/navigation'
    import { revalidateTag } from 'next/cache'
     
    export async function createPost(id: string) {
      try {
        // ...
      } catch (error) {
        // ...
      }
     
      revalidateTag('posts') // Update cached posts
      redirect(`/post/${id}`) // Navigate to the new post page
    }

    Cookie

    您可以在伺服器動作中使用 cookies API 來 取得設定刪除 Cookie

    app/actions.ts
    'use server'
     
    import { cookies } from 'next/headers'
     
    export async function exampleAction() {
      const cookieStore = await cookies()
     
      // Get cookie
      cookieStore.get('name')?.value
     
      // Set cookie
      cookieStore.set('name', 'Delba')
     
      // Delete cookie
      cookieStore.delete('name')
    }

    有關從伺服器動作中刪除 Cookie 的更多範例,請參閱 其他範例

    安全性

    根據預設,建立並匯出伺服器動作時,它會建立一個公開的 HTTP 端點,並且應採用相同的安全性假設和授權檢查。這表示,即使伺服器動作或工具函式未在程式碼的其他位置匯入,它仍然可以公開存取。

    為了提升安全性,Next.js 具有以下內建功能:

    • 安全的動作 ID:Next.js 會建立加密的、非確定性 ID,讓用戶端可以參考和呼叫伺服器動作。這些 ID 會在每次建置之間定期重新計算,以增強安全性。
    • 無用程式碼移除:未使用的伺服器動作(由其 ID 參照)會從用戶端套件組合中移除,以避免第三方公開存取。

    注意事項:

    ID 會在編譯期間建立,並最多快取 14 天。當新的建置啟動或建置快取失效時,它們將會重新產生。這項安全性改進降低了缺少驗證層時發生的風險。但是,您仍然應該將伺服器動作視為公開的 HTTP 端點。

    // app/actions.js
    'use server'
     
    // This action **is** used in our application, so Next.js
    // will create a secure ID to allow the client to reference
    // and call the Server Action.
    export async function updateUserAction(formData) {}
     
    // This action **is not** used in our application, so Next.js
    // will automatically remove this code during `next build`
    // and will not create a public endpoint.
    export async function deleteUserAction(formData) {}

    驗證與授權

    您應該確保使用者已獲得執行該動作的授權。例如:

    app/actions.ts
    'use server'
     
    import { auth } from './lib'
     
    export function addItem() {
      const { user } = auth()
      if (!user) {
        throw new Error('You must be signed in to perform this action')
      }
     
      // ...
    }

    閉包與加密

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

    app/page.tsx
    export default async function Page() {
      const publishVersion = await getLatestVersion();
     
      async function publish() {
        "use server";
        if (publishVersion !== await getLatestVersion()) {
          throw new Error('The version has changed since pressing publish');
        }
        ...
      }
     
      return (
        <form>
          <button formAction={publish}>Publish</button>
        </form>
      );
    }

    當您需要在渲染時擷取資料的「快照」(例如 publishVersion),以便稍後在呼叫動作時使用時,閉包會很有用。

    然而,為了實現這一點,擷取的變數會在呼叫動作時傳送到用戶端,然後再傳回伺服器。為了防止敏感資料暴露給用戶端,Next.js 會自動加密閉包變數。每次建置 Next.js 應用程式時,都會為每個動作產生一個新的私鑰。這表示動作只能針對特定建置版本呼叫。

    注意事項:我們不建議僅依靠加密來防止敏感值暴露在用戶端上。相反地,您應該使用React taint API 來主動防止特定資料傳送到用戶端。

    覆寫加密金鑰(進階)

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

    為了減輕這種情況,您可以使用 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 環境變數覆寫加密金鑰。指定此變數可確保您的加密金鑰在不同建置版本之間保持一致,並且所有伺服器執行個體都使用相同的金鑰。

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

    貼心小提醒:部署到 Vercel 的 Next.js 應用程式會自動處理這個問題。

    允許的來源(進階)

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

    在幕後,伺服器動作使用 POST 方法,而且只有這個 HTTP 方法允許呼叫它們。這可以防止現代瀏覽器中大多數的 CSRF 漏洞,尤其是在預設使用 SameSite Cookie 的情況下。

    作為額外的保護,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 文件

    下一步 (下一步)

    瞭解如何在 Next.js 中設定伺服器動作