若要執行更進階的伺服器端驗證,您可以使用像是 zod 這樣的函式庫,在變更資料前先驗證表單欄位。
'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 的函式簽名會改變,將接收一個新的 prevState
或 initialState
參數作為其第一個引數。
useFormState
是一個 React hook,因此必須在 Client Component(客戶端元件)中使用。
'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
顯示錯誤訊息。
'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 執行時顯示載入指示器。
'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,而無需等待回應。
'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
)來呼叫。例如,要增加按讚數:
'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
時儲存表單欄位。
'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
hook,在元件掛載或依賴項變更時呼叫伺服器動作。這對於依賴於全域事件或需要自動觸發的變更操作很有用。例如,應用程式快捷鍵的 onKeyDown
、無限捲動的交叉觀察器 hook,或是在元件掛載時更新瀏覽次數。
'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 處理。
例如,您的伺服器動作可能會透過回傳訊息來處理建立新項目的錯誤。
'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 快取
'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') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}
您可以在伺服器動作中使用 cookies
API 來 取得
、設定
和 刪除
Cookie
'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) {}
您應該確保使用者已獲得執行該動作的授權。例如:
'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
變數。
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
來指定安全來源清單。此選項接受字串陣列。
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}
深入瞭解 安全性與伺服器動作。
如需更多資訊,請查看下列 React 文件