Chuyển đến nội dung chính

Bài 11: Server Actions & Mutations

Server Actions, "use server". Form actions, progressive enhancement. Optimistic updates, useActionState. Error handling, revalidation sau mutation.

💻 Lập trình — Bài 11 Bài 11: Server Actions & Mutations

React & Next.js: Từ Cơ bản đến Nâng cao

Phần 3: Next.js Fundamentals

xdev.asia

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

FeatureServer ActionsAPI Routes
Progressive enhancement✅ Works without JS❌ Requires JS
Type safety✅ End-to-end⚠️ Manual
Revalidation✅ Built-in⚠️ Manual
Use caseMutations từ UIExternal API, webhooks

Bài tiếp theo: Authentication với NextAuth.js — đăng nhập, phân quyền, session management.