📋 目錄
React Server Components(RSC)和 Server Actions 是 React 生態系在 2024-2026 年最重要的架構演進。它們改變了「前後端」之間的邊界——伺服器不再只是 API 端點,而是可以直接參與 React 組件的渲染邏輯。對於前端工程師來說,這是一個需要重新思考「哪段代碼該在哪裡執行」的機會。讓我們來看看 2026 年的 Server Actions 最佳實踐。
Server Actions 是什麼
Server Actions 是在伺服器端執行的非同步函數,Client Components(客戶端組件)可以直接呼叫它們:
// app/actions.ts
'use server';
// 這個函數在伺服器執行
export async function submitForm(formData: FormData) {
// 可以直接訪問資料庫
const email = formData.get('email');
const response = await db.insert({ email }).into('subscribers');
return { success: true, id: response.id };
}
// app/page.tsx
'use client';
import { submitForm } from './actions';
export default function Form() {
return (
<form action={submitForm}>
<input name="email" type="email" />
<button type="submit">Subscribe</button>
</form>
);
}
這個模式消滅了 API 路由的 boilerplate——你不再需要寫 pages/api/subscribe.ts 或 app/api/subscribe/route.ts。
為什麼 Server Actions 值得關注
減少 Boilerplate
| 傳統 API Routes | Server Actions |
|---|---|
app/api/subscribe/route.ts | 直接在 actions.ts |
POST handler + validation | 同一個函數 |
useEffect + fetch | <form action={...}> |
| 來回 HTTP 請求 | 直接函數呼叫 |
類型安全
Server Actions 的參數和返回值可以完全享受 TypeScript 的類型檢查:
// app/actions.ts
'use server';
interface SubscribeResult {
success: boolean;
id?: string;
error?: string;
}
export async function subscribe(
email: string
): Promise<SubscribeResult> {
if (!email.includes('@')) {
return { success: false, error: 'Invalid email' };
}
const result = await db.insert({ email });
return { success: true, id: result.id };
}
// 在 Client Component 中調用,類型自動推斷
const result = await subscribe(userInput);
if (result.error) {
// TypeScript 知道 result.error 存在
showError(result.error);
}
Suspense Boundaries:處理載入狀態
Server Actions 的一大挑戰是:伺服器執行意味著回應時間不確定。Suspense Boundaries 是 React 提供的解決方案。
基本用法
// app/page.tsx
import { Suspense } from 'react';
import { UserList } from './UserList';
import { UserListSkeleton } from './UserListSkeleton';
export default function Page() {
return (
<div>
<h1>Users</h1>
{/* 慢伺服器組件會觸發 loading UI */}
<Suspense fallback={<UserListSkeleton />}>
<UserList />
</Suspense>
</div>
);
}
多層 Suspense
export default function Dashboard() {
return (
<div>
{/* 每個區塊獨立的 Suspense */}
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}
每個 <Suspense> 邊界可以獨立載入,這讓頁面的感知效能大幅提升——用戶可以看到頁面「分塊」出現,而不是等整個頁面。
useOptimistic:樂觀更新
useOptimistic 是 React 19 引入的 Hook,專門用來處理「在伺服器回應前,先更新 UI」的場景。
典型場景:按讚
'use client';
import { useOptimistic, useTransition } from 'react';
import { toggleLike } from './actions';
type LikeButtonProps = {
initialLikes: number;
initialHasLiked: boolean;
postId: string;
};
export function LikeButton({
initialLikes,
initialHasLiked,
postId,
}: LikeButtonProps) {
const [optimisticState, addOptimistic] = useOptimistic(
{ likes: initialLikes, hasLiked: initialHasLiked },
// 樂觀更新函數
(state, newHasLiked: boolean) => ({
likes: state.likes + (newHasLiked ? 1 : -1),
hasLiked: newHasLiked,
})
);
const [isPending, startTransition] = useTransition();
const handleClick = async () => {
const newHasLiked = !optimisticState.hasLiked;
// 立即更新 UI(樂觀)
addOptimistic(newHasLiked);
// 背景發送伺服器請求
startTransition(async () => {
await toggleLike(postId, newHasLiked);
});
};
return (
<button onClick={handleClick} disabled={isPending}>
{optimisticState.hasLiked ? '❤️' : '🤍'} {optimisticState.likes}
</button>
);
}
用戶點擊按鈕的瞬間,UI 就更新了——不需要等伺服器回應。如果請求失敗,React 會自動回滾到正確狀態。
useFormStatus:表單提交狀態
useFormStatus 提供表單提交的狀態資訊,特別適合用在表單內部的提交按鈕:
// app/submit-button.tsx
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
);
}
// app/form.tsx
import { SubmitButton } from './submit-button';
export function ContactForm() {
return (
<form action={async (formData) => {
'use server';
await sendEmail(formData);
}}>
<input name="email" type="email" />
<textarea name="message" />
{/* SubmitButton 可以讀取所屬表單的狀態 */}
<SubmitButton />
</form>
);
}
安全性最佳實踐
Server Actions 的安全性容易被忽略,因為它看起來像普通的函數呼叫。以下是必須注意的事項:
1. 總是驗證用戶輸入
'use server';
export async function submitOrder(formData: FormData) {
// ❌ 永遠不要信任 client 端的資料
const userId = formData.get('userId'); // 可以被偽造
// ✅ 從 session/cookie 獲取真實的 userId
const session = await getSession();
const userId = session.user.id;
// ✅ 驗證所有輸入
const productId = formData.get('productId');
const quantity = parseInt(formData.get('quantity') as string, 10);
if (quantity < 1 || quantity > 100) {
return { error: 'Invalid quantity' };
}
// ✅ 授權檢查
const product = await db.products.find(productId);
if (!product) {
return { error: 'Product not found' };
}
const order = await db.orders.create({
userId: session.user.id,
productId,
quantity,
});
return { success: true, orderId: order.id };
}
2. 使用 Zod 進行 Schema 驗證
import { z } from 'zod';
const SubscribeSchema = z.object({
email: z.string().email('Invalid email address'),
name: z.string().min(2, 'Name too short').max(100),
});
'use server';
export async function subscribe(formData: FormData) {
const rawData = {
email: formData.get('email'),
name: formData.get('name'),
};
// Zod 驗證 + TypeScript 類型推斷
const result = SubscribeSchema.safeParse(rawData);
if (!result.success) {
return {
error: result.error.flatten().fieldErrors,
};
}
const { email, name } = result.data;
// ... 執行實際邏輯
}
3. Rate Limiting
import { ratelimit } from './ratelimit';
'use server';
export async function submitComment(formData: FormData) {
const { success } = await ratelimit.limit('comments');
if (!success) {
return { error: 'Too many requests. Please wait.' };
}
// ... 處理評論提交
}
Server Actions vs API Routes:何時用哪個
| 場景 | Server Actions ✅ | API Routes |
|---|---|---|
| 表單提交 | ✅ 直接對應 | 需要額外處理 |
| 資料庫 CRUD | ✅ 直接存取 | 需要 API 層 |
| 第三方 API 呼叫 | ✅ | ✅ |
| Webhook | ❌ | ✅ |
| 需要 middleware | ❌ | ✅ |
| 公開 API(供外部使用) | ❌ | ✅ |
結語:重新思考前後端邊界
Server Actions 代表的不只是一個新語法,而是 React 生態系對「前後端分離」這個概念的重新思考。
過去,前端工程師寫 JavaScript,後端工程師寫 API,兩者之間有明確的邊界。Server Actions 模糊了這個邊界——你可以在 React 組件裡直接呼叫伺服器函數,不需要 HTTP 請求,不需要 useEffect。
這個變化對前端工程師提出了新的要求:你不能再假設所有代碼都跑在瀏覽器裡。你需要理解伺服器環境的限制(不能訪問 window、document),也需要更嚴肅地對待安全性——攻擊者可以直接操控你的 Server Action 參數。
但這個變化也帶來了真正的簡化:更少的 boilerplate、更直接的資料流、更天然的 TypeScript 支援。
延伸閱讀
- 2026 前端框架趨勢:Signal 時代來臨 — 前端框架最新趨勢
- Next.js 完整教學 — Next.js 最新版本功能
本文是「2026 React 進階技術」系列文章之一。