Next.js 15 Server Actions 表單處理完整教學:從基礎到進階實戰(2026)
Next.js 15 把 Server Actions 推到了一個相當成熟的階段。如果你還在用 API Routes 處理表單提交,說真的,是時候升級你的開發方式了。Server Actions 讓你可以直接在 Server Component 裡面定義伺服器端的邏輯,不用再另外寫 API endpoint,整個開發流程順暢到會讓你回不去。
我自己把幾個專案從 API Routes 遷移到 Server Actions 之後,程式碼量大概減少了 30%,而且更直覺、更好維護。今天這篇就來完整走一遍 Server Actions 在表單處理上的各種用法。
什麼是 Server Actions?
簡單來說,Server Actions 是可以在伺服器端執行的非同步函式。你只要在函式最上面加上 'use server' 指令,Next.js 就會自動幫你處理 Client-Server 之間的通訊。不用寫 fetch、不用定義 API route、不用處理 CORS——它全包了。
跟傳統的做法比較一下:
// 以前要這樣寫 API Route + Client fetch
// pages/api/submit.ts
export default async function handler(req, res) {
const data = JSON.parse(req.body);
await db.insert(data);
res.json({ success: true });
}
// 現在用 Server Actions
// app/actions.ts
'use server'
export async function submitForm(formData: FormData) {
const name = formData.get('name');
await db.insert({ name });
}少了多少程式碼,一目了然。而且 Server Actions 跟React 19 的 useActionState 和 useOptimistic 完美搭配,表單的狀態管理變得前所未有的簡單。
基礎表單提交
來看最基本的用法。先建立一個 Server Action:
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// 儲存到資料庫
await db.posts.create({
data: { title, content }
})
// 重新驗證頁面快取
revalidatePath('/posts')
}然後在表單中使用:
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input type="text" name="title" placeholder="文章標題" required />
<textarea name="content" placeholder="文章內容" required />
<button type="submit">發布文章</button>
</form>
)
}注意到了嗎?這是一個 Server Component,完全不需要 'use client'。表單提交時,Next.js 會自動把 FormData 送到 Server Action 處理。更厲害的是,即使 JavaScript 載入失敗,這個表單照樣能用——因為它本質上就是一個標準的 HTML form。
表單驗證:用 Zod 做伺服器端驗證
光是能提交表單還不夠,驗證才是重點。我推薦用 Zod 來做 schema 驗證:
// app/actions.ts
'use server'
import { z } from 'zod'
const PostSchema = z.object({
title: z.string().min(3, '標題至少 3 個字').max(100, '標題不超過 100 個字'),
content: z.string().min(10, '內容至少 10 個字'),
category: z.enum(['tech', 'design', 'life'], {
errorMap: () => ({ message: '請選擇有效的分類' }),
}),
})
export type FormState = {
errors?: Record<string, string[]>
message?: string
success?: boolean
}
export async function createPost(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const validated = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
category: formData.get('category'),
})
if (!validated.success) {
return {
errors: validated.error.flatten().fieldErrors,
message: '表單驗證失敗',
}
}
try {
await db.posts.create({ data: validated.data })
return { success: true, message: '文章發布成功!' }
} catch (error) {
return { message: '發生錯誤,請稍後再試' }
}
}搭配 useActionState 處理表單狀態
有了驗證邏輯之後,前端需要顯示錯誤訊息。這時候就輪到 useActionState 出場了:
// app/posts/new/form.tsx
'use client'
import { useActionState } from 'react'
import { createPost, type FormState } from '@/app/actions'
const initialState: FormState = {}
export function PostForm() {
const [state, formAction, isPending] = useActionState(createPost, initialState)
return (
<form action={formAction}>
<div>
<input type="text" name="title" placeholder="文章標題" />
{state.errors?.title && (
<p className="text-red-500">{state.errors.title[0]}</p>
)}
</div>
<div>
<textarea name="content" placeholder="文章內容" />
{state.errors?.content && (
<p className="text-red-500">{state.errors.content[0]}</p>
)}
</div>
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '發布文章'}
</button>
{state.message && <p>{state.message}</p>}
</form>
)
}這邊要注意,因為用到了 useActionState 這個 Hook,所以這個元件需要標記為 'use client'。但 Server Action 本身還是在伺服器執行的,安全性沒問題。
樂觀更新:useOptimistic 即時回饋
使用者體驗的關鍵之一就是回饋速度。useOptimistic 讓你在 Server Action 還沒完成之前,就先在 UI 上顯示預期的結果:
'use client'
import { useOptimistic } from 'react'
import { addComment } from '@/app/actions'
export function CommentList({ comments }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment: string) => [
...state,
{ id: 'temp', text: newComment, pending: true }
]
)
async function handleSubmit(formData: FormData) {
const text = formData.get('comment') as string
addOptimisticComment(text)
await addComment(formData)
}
return (
<>
{optimisticComments.map(comment => (
<div key={comment.id} style={{ opacity: comment.pending ? 0.5 : 1 }}>
{comment.text}
</div>
))}
<form action={handleSubmit}>
<input name="comment" />
<button type="submit">送出留言</button>
</form>
</>
)
}用半透明顯示還在等待確認的留言,給使用者一種「秒回」的感覺,這在React Server Components 架構中是非常推薦的模式。
檔案上傳處理
Server Actions 也能處理檔案上傳,FormData 天生就支援:
'use server'
export async function uploadImage(formData: FormData) {
const file = formData.get('image') as File
if (!file || file.size === 0) {
return { error: '請選擇檔案' }
}
if (file.size > 5 * 1024 * 1024) {
return { error: '檔案大小不能超過 5MB' }
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
// 上傳到雲端儲存(S3、Supabase Storage 等)
const url = await uploadToStorage(buffer, file.name)
return { success: true, url }
}安全性最佳實踐
Server Actions 在伺服器端執行,安全性相對好,但還是有幾點要注意:
- 永遠做伺服器端驗證:客戶端驗證是為了體驗,伺服器端驗證才是為了安全
- 權限檢查:每個 Action 都要檢查使用者身份和權限
- Rate Limiting:防止惡意大量提交
- CSRF 防護:Next.js 內建了 CSRF token 機制,不需要額外處理
'use server'
import { auth } from '@/lib/auth'
export async function deletePost(postId: string) {
const session = await auth()
if (!session?.user) {
throw new Error('未登入')
}
const post = await db.posts.findUnique({ where: { id: postId } })
if (post?.authorId !== session.user.id) {
throw new Error('沒有權限刪除此文章')
}
await db.posts.delete({ where: { id: postId } })
revalidatePath('/posts')
}關於測試 Server Actions,我建議搭配Vitest 單元測試來確保每個 Action 的行為正確。
快取與重新驗證策略
Server Actions 執行完之後,通常需要更新頁面上的資料。Next.js 提供了兩種方式:
revalidatePath('/path'):重新驗證特定路徑的快取revalidateTag('tag'):重新驗證帶有特定 tag 的快取
搭配 Next.js 15 的use cache 與 cacheTag 策略,你可以精確控制哪些資料需要在什麼時候刷新,避免不必要的重新渲染。
總結:Server Actions 是 Next.js 表單處理的未來
回顧一下今天學到的重點:Server Actions 讓表單處理變得極度簡潔,配合 Zod 做驗證、useActionState 管理狀態、useOptimistic 做樂觀更新,整套流程又安全又流暢。
如果你的專案還在用 Next.js 14 以前的版本,我真的建議儘快升級到 15。Server Actions 在 15 裡面已經是 stable API,效能跟開發體驗都比之前好很多。先從一個簡單的表單開始改,感受一下差異,你就會想把所有表單都遷移過去了。
繼續閱讀
Next.js 16 View Transitions API 實戰:用原生轉場動畫打造絲滑頁面切換體驗
相關文章
你可能也喜歡
探索其他領域的精選好文