1. Server Actions là gì?
Server Actions là các hàm async chạy trên server, được gọi từ client thông qua form hoặc event handler. Thay thế API routes cho mutations.
// 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. Form Actions
// 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. useActionState
'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. Validation với 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. Optimistic Updates
'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. Delete & Inline Actions
// 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. So sánh Server Actions vs API Routes
| Feature | Server Actions | API Routes |
|---|---|---|
| Progressive enhancement | ✅ Works without JS | ❌ Requires JS |
| Type safety | ✅ End-to-end | ⚠️ Manual |
| Revalidation | ✅ Built-in | ⚠️ Manual |
| Use case | Mutations từ UI | External API, webhooks |
Bài tiếp theo: Authentication với NextAuth.js — đăng nhập, phân quyền, session management.