Server Actions cho phép form gọi thẳng hàm chạy trên máy chủ mà không cần tự viết API. Bài này hướng dẫn tạo form thêm sinh viên, kiểm tra dữ liệu bằng Zod, hiển thị lỗi với useActionState, trạng thái gửi, làm mới dữ liệu và chuyển hướng.
Bạn sẽ học được
- Viết Server Action với "use server"
- Kiểm tra dữ liệu bằng Zod
- Hiển thị lỗi, trạng thái đang gửi
- revalidatePath và redirect sau khi lưu
Một số khái niệm
Server Action là hàm async có chỉ thị 'use server'. Gắn vào <form action={ham}>, Next.js tự gửi dữ liệu (FormData) lên máy chủ, chạy hàm và trả kết quả. Form vẫn hoạt động ngay cả khi JavaScript chưa tải xong.
Server Action
// src/app/sinhvien/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
const SinhVien = z.object({
ma_sv: z.string().regex(/^\d{8}$/, 'Mã gồm 8 chữ số'),
ho_ten: z.string().min(2, 'Họ tên quá ngắn'),
email: z.email('Email không hợp lệ'),
});
export type TrangThai = { loi?: Record<string, string[]>; thongBao?: string };
export async function themSinhVien(_: TrangThai, formData: FormData): Promise<TrangThai> {
const kq = SinhVien.safeParse(Object.fromEntries(formData));
if (!kq.success) return { loi: z.flattenError(kq.error).fieldErrors };
await db.sinhVien.create({ data: kq.data });
revalidatePath('/sinhvien');
redirect('/sinhvien');
}Form phía client
// src/app/sinhvien/them/FormThem.tsx
'use client';
import { useActionState } from 'react';
import { themSinhVien } from '../actions';
export default function FormThem() {
const [state, action, dangGui] = useActionState(themSinhVien, {});
return (
<form action={action} className="max-w-md space-y-3">
<input name="ma_sv" placeholder="Mã SV" className="w-full rounded border p-2" />
<p className="text-sm text-red-600">{state.loi?.ma_sv?.[0]}</p>
<input name="ho_ten" placeholder="Họ tên" className="w-full rounded border p-2" />
<p className="text-sm text-red-600">{state.loi?.ho_ten?.[0]}</p>
<input name="email" type="email" placeholder="Email" className="w-full rounded border p-2" />
<p className="text-sm text-red-600">{state.loi?.email?.[0]}</p>
<button disabled={dangGui} className="rounded bg-emerald-600 px-4 py-2 text-white disabled:opacity-50">
{dangGui ? 'Đang lưu...' : 'Lưu'}
</button>
</form>
);
}Xóa bằng action
// Truyền id bằng bind
import { xoaSinhVien } from './actions';
<form action={xoaSinhVien.bind(null, sv.id)}>
<button className="text-red-600">Xóa</button>
</form>
// actions.ts
export async function xoaSinhVien(id: number) {
await kiemTraQuyen('admin'); // luôn kiểm tra quyền trong action!
await db.sinhVien.delete({ where: { id } });
revalidatePath('/sinhvien');
}Mẹo hay
Kết hợp useOptimistic để cập nhật giao diện ngay (ví dụ đánh dấu đã xóa) trước khi máy chủ trả lời.
Lưu ý
Server Action là một điểm cuối công khai: ai cũng có thể gọi. Luôn kiểm tra đăng nhập, quyền và dữ liệu bên trong action, không tin vào việc nút bị ẩn trên giao diện.
Thực hành
- Tạo form thêm sinh viên với Server Action.
- Hiển thị lỗi từng trường bằng useActionState.
- Thêm nút xóa có hộp xác nhận.
- Tạo form sửa sinh viên, điền sẵn dữ liệu cũ.
Tóm tắt bài học
- "use server" tạo hàm chạy trên máy chủ.
- form action={...} gửi FormData tự động.
- Zod kiểm tra, useActionState hiện lỗi và trạng thái.
- revalidatePath, redirect; luôn kiểm tra quyền.