1. 什麼是伺服器操作?
伺服器操作是在伺服器上執行的非同步函數,透過表單或事件處理程序從客戶端呼叫。替換突變的 API 路由。
// actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({
data: { title, content },
});
revalidatePath('/posts');
redirect('/posts');
}
2. 表單操作
// Server Component — no JS needed on client
import { createPost } from './actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required placeholder="Tiêu đề" />
<textarea name="content" required placeholder="Nội dung" />
<button type="submit">Tạo bài viết</button>
</form>
);
}
3. 使用動作狀態
'use client';
import { useActionState } from 'react';
import { createPost } from './actions';
type State = {
errors?: { title?: string; content?: string };
message?: string;
};
export function PostForm() {
const [state, formAction, isPending] = useActionState<State, FormData>(
createPost,
{ errors: {}, message: '' }
);
return (
<form action={formAction}>
<input name="title" />
{state.errors?.title && <p className="text-red-500">{state.errors.title}</p>}
<textarea name="content" />
{state.errors?.content && <p className="text-red-500">{state.errors.content}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Đang tạo...' : 'Tạo bài viết'}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}
4. 使用 Zod 進行驗證
// actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
const PostSchema = z.object({
title: z.string().min(3, 'Tiêu đề ít nhất 3 ký tự').max(100),
content: z.string().min(10, 'Nội dung ít nhất 10 ký tự'),
});
export async function createPost(prevState: State, formData: FormData) {
const parsed = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
message: 'Validation failed',
};
}
try {
await db.post.create({ data: parsed.data });
revalidatePath('/posts');
return { message: 'Tạo thành công!' };
} catch {
return { message: 'Có lỗi xảy ra' };
}
}
5. 樂觀的更新
'use client';
import { useOptimistic } from 'react';
import { toggleLike } from './actions';
export function LikeButton({ liked, count }: { liked: boolean; count: number }) {
const [optimistic, addOptimistic] = useOptimistic(
{ liked, count },
(state, newLiked: boolean) => ({
liked: newLiked,
count: newLiked ? state.count + 1 : state.count - 1,
})
);
return (
<form action={async () => {
addOptimistic(!optimistic.liked);
await toggleLike();
}}>
<button type="submit">
{optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
</button>
</form>
);
}
6. 刪除和內聯操作
// Inline server action with bind
import { deletePost } from './actions';
export default async function PostList() {
const posts = await db.post.findMany();
return (
<ul>
{posts.map(post => {
const deleteWithId = deletePost.bind(null, post.id);
return (
<li key={post.id}>
{post.title}
<form action={deleteWithId}>
<button type="submit">Xóa</button>
</form>
</li>
);
})}
</ul>
);
}
7. 比較伺服器操作與 API 路由
| 特點 | 伺服器操作 | API 路由 |
|---|---|---|
| 漸進增強 | ✅ 無需 JS 即可運作 | ❌需要JS |
| 類型安全 | ✅ 端到端 | ⚠️手冊 |
| 重新驗證 | ✅ 內置 | ⚠️手冊 |
| 使用案例 | UI 的突變 | 外部 API、網路鉤子 |
下一篇: 使用 NextAuth.js 進行身份驗證 — 登入、授權、會話管理。