跳到內容
建置你的應用程式資料擷取伺服器行為與變更

伺服器行為與變更

伺服器行為 是在伺服器上執行的非同步函式。它們可以在伺服器和用戶端元件中呼叫,以處理 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/button.tsx
'use client'
 
import { create } from './actions'
 
export function Button() {
  return <button onClick={() => create()}>Create</button>
}

將行為作為 props 傳遞

您也可以將伺服器行為作為 prop 傳遞至用戶端元件

<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 外掛程式實際上不知道它是否接收到伺服器行為或普通函式。執行階段類型檢查仍將確保您不會意外地將函式傳遞至用戶端元件。

行為

  • 伺服器行為可以使用 <form> 元素中的 action 屬性來調用
    • 伺服器元件預設支援漸進式增強,這表示即使 JavaScript 尚未載入或已停用,表單仍會提交。
    • 在用戶端元件中,調用伺服器行為的表單如果 JavaScript 尚未載入,將會將提交排入佇列,優先處理用戶端 hydration。
    • hydration 後,瀏覽器不會在表單提交時重新整理。
  • 伺服器行為不限於 <form>,可以從事件處理器、useEffect、第三方函式庫和其他表單元素 (如 <button>) 調用。
  • 伺服器行為與 Next.js 快取和重新驗證 架構整合。當調用行為時,Next.js 可以在單個伺服器往返中傳回更新的 UI 和新資料。
  • 在幕後,行為使用 POST 方法,且只有此 HTTP 方法可以調用它們。
  • 伺服器行為的引數和傳回值必須可由 React 序列化。請參閱 React 文件,以取得 可序列化的引數和值的清單。
  • 伺服器行為是函式。這表示它們可以在應用程式中的任何位置重複使用。
  • 伺服器行為繼承使用它們的頁面或版面配置的 執行階段
  • 伺服器行為繼承使用它們的頁面或版面配置的 路由區段設定,包括 maxDuration 等欄位。

範例

表單

React 擴展了 HTML <form> 元素,以允許使用 action prop 調用伺服器行為。

在表單中調用時,行為會自動接收 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.ts
'use server'
 
export async function updateUser(userId: string, formData: FormData) {}

要知道的好資訊:

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

巢狀表單元素

您也可以在巢狀於 <form> 內的元素 (例如 <button><input type="submit"><input type="image">) 中調用伺服器行為。這些元素接受 formAction prop 或 事件處理器

如果您想要在表單中呼叫多個伺服器行為,這會很有用。例如,您可以建立一個特定的 <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> 祖先的提交,這將調用伺服器行為。

伺服器端表單驗證

您可以使用 HTML 屬性 (例如 requiredtype="email") 進行基本用戶端表單驗證。

對於更進階的伺服器端驗證,您可以使用 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
}

在伺服器上驗證欄位後,您可以傳回伺服器行為中可序列化的物件,並使用 React useActionState Hook 向使用者顯示訊息。

  • 透過將行為傳遞至 useActionState,行為的函式簽名會變更為接收新的 prevStateinitialState 參數作為其第一個引數。
  • useActionState 是 React Hook,因此必須在用戶端元件中使用。
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')
}

然後,您可以將您的行為傳遞至 useActionState Hook,並使用傳回的 state 來顯示錯誤訊息。

app/ui/signup.tsx
'use client'
 
import { useActionState } from 'react'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
export function Signup() {
  const [state, formAction, pending] = useActionState(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 disabled={pending}>Sign up</button>
    </form>
  )
}

待處理狀態

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

或者,您可以使用 useFormStatus Hook,以在執行行為時顯示載入指示器。當使用此 Hook 時,您需要建立一個單獨的元件來渲染載入指示器。例如,在行為待處理時停用按鈕

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

然後,您可以將 SubmitButton 元件巢狀於表單內

app/ui/signup.tsx
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
 
export function Signup() {
  return (
    <form action={createUser}>
      {/* Other form elements */}
      <SubmitButton />
    </form>
  )
}

要知道的好資訊: 在 React 19 中,useFormStatus 在傳回的物件上包含其他索引鍵,例如 data、method 和 action。如果您未使用 React 19,則只有 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: 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>
  )
}

對於這種情況,可能會快速連續觸發多個事件,我們建議進行防抖,以防止不必要的伺服器行為調用。

useEffect

您可以使用 React useEffect Hook 在元件掛載或依賴項變更時調用伺服器行為。這對於依賴全域事件或需要自動觸發的變更很有用。例如,應用程式快捷鍵的 onKeyDown、無限捲動的 intersection observer 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> 邊界捕獲。請參閱 錯誤處理 以取得更多資訊。

要知道的好資訊

重新驗證資料

您可以使用 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)

您可以使用 cookies API,在伺服器行為 (Server Action) 內 getsetdelete 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 環境變數覆寫加密金鑰。指定此變數可確保您的加密金鑰在建置之間保持持久性,且所有伺服器執行個體都使用相同的金鑰。此變數必須經過 AES-GCM 加密。

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

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

允許的來源 (進階)

由於伺服器行為可以在 <form> 元素中呼叫,這會使其容易受到 CSRF 攻擊

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

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

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

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

深入了解安全性與伺服器行為

其他資源

如需更多資訊,請查看以下 React 文件

下一步

了解如何在 Next.js 中設定伺服器行為