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

Lesson 11: Server Actions & Mutations

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

💻 Programming — Lesson 11 Lesson 11: Server Actions & Mutations

React & Next.js: From Basics to Advanced

Part 3: Next.js Fundamentals

xdev.asia

1. What are Server Actions?

Server Actions are async functions running on the server, called from the client via form or event handler. Replace API routes for 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 with 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. Compare Server Actions vs API Routes

FeaturesServer ActionsAPI Routes
Progressive enhancement✅ Works without JS❌ Requires JS
Type safety✅ End-to-end⚠️ Manual
Revalidation✅ Built-in⚠️ Manual
Use casesMutations from UIExternal API, webhooks

Next article: Authentication with NextAuth.js — login, authorization, session management.