[Next.js Tutorial] Server Actions & Mutations: Hướng dẫn áp dụng với ví dụ thực tế

Trong thế giới phát triển web hiện đại với Next.js, Server Actions và Mutations nổi lên như một cuộc cách mạng, thay đổi hoàn toàn cách chúng ta tương tác và thay đổi dữ liệu phía server. Hãy quên đi những API routes rườm rà và các lệnh gọi fetch lặp đi lặp lại. Giờ đây, bạn có thể gọi trực tiếp các hàm chạy trên server từ trong component của mình, mang lại trải nghiệm lập trình liền mạch, hiệu quả và an toàn hơn bao giờ hết.

Server Actions & Mutations trong Next.js

Bài viết này sẽ giúp bạn không chỉ hiểu rõ "Server Actions và Mutations là gì?", mà còn nắm vững cách triển khai chúng một cách hiệu quả trong các dự án Next.js của mình.

1. Server Actions: "Cánh tay nối dài" của component đến Server

Hiểu một cách đơn giản, Server Actions là các hàm bất đồng bộ (async function) được bạn định nghĩa và thực thi trực tiếp trên server, nhưng lại có thể được gọi từ cả Server Components và Client Components. Điều này mở ra một luồng làm việc cực kỳ mạnh mẽ: thay vì phải tạo một API endpoint riêng biệt chỉ để xử lý một hành động (ví dụ: đăng ký email, thêm sản phẩm vào giỏ hàng), bạn có thể gói gọn logic đó vào một hàm và gọi nó trực tiếp.

Để "đánh dấu" một hàm là Server Action, bạn chỉ cần thêm directive "use server"; ở đầu hàm hoặc ở đầu file chứa hàm đó.

Ví dụ cơ bản với form:

Hãy tưởng tượng bạn có một form đơn giản để người dùng thêm một công việc mới vào danh sách.

// app/page.tsx

export default function TodoPage() {
  async function addTodo(formData: FormData) {
    'use server'; // Đánh dấu đây là một Server Action

    const todo = formData.get('todo');
    // Logic để lưu 'todo' vào cơ sở dữ liệu
    console.log(`Đã thêm công việc mới: ${todo}`);
  }

  return (
    <form action={addTodo}>
      <input type="text" name="todo" />
      <button type="submit">Thêm công việc</button>
    </form>
  );
}

Trong ví dụ trên:

  • Hàm addTodo được định nghĩa ngay bên trong component.
  • Với directive "use server";, Next.js biết rằng hàm này phải được thực thi trên server.
  • Thuộc tính action của thẻ <form> trỏ thẳng đến hàm addTodo. Khi form được submit, Next.js sẽ tự động gọi Server Action này, truyền vào đối tượng FormData chứa dữ liệu của form.

Điều kỳ diệu ở đây là bạn không cần viết bất kỳ dòng mã fetch hay API route nào. Mọi thứ diễn ra một cách "ma thuật" nhưng hoàn toàn có thể đoán trước được.

2. Mutations: Khi hành động thay đổi dữ liệu

Trong ngữ cảnh của Server Actions, Mutation không phải là một khái niệm hay cú pháp riêng biệt, mà chính là mục đích sử dụng của Server Action đó: để thay đổi dữ liệu (tạo mới, cập nhật, hoặc xóa - CUD trong CRUD). Hàm addTodo ở trên chính là một ví dụ điển hình của mutation.

Sức mạnh thực sự của mutations với Server Actions nằm ở sự tích hợp sâu sắc với hệ thống caching và revalidation của Next.js. Sau khi một mutation thành công, bạn thường muốn giao diện người dùng được cập nhật để phản ánh sự thay đổi đó. Next.js cung cấp các hàm tiện ích như revalidatePathrevalidateTag để giải quyết vấn đề này một cách thanh lịch.

Ví dụ nâng cao với Revalidation:

Hãy cải tiến ví dụ TodoPage để hiển thị danh sách công việc và tự động cập nhật sau khi thêm mới.

// app/page.tsx
import { revalidatePath } from 'next/cache';

// Giả sử hàm này lấy dữ liệu từ database
async function getTodos() {
  // ... logic lấy danh sách công việc
  return ['Học Next.js', 'Làm dự án mới'];
}

export default async function TodoPage() {
  const todos = await getTodos();

  async function addTodo(formData: FormData) {
    'use server';

    const todo = formData.get('todo');
    // ... logic lưu 'todo' vào cơ sở dữ liệu

    // Yêu cầu Next.js revalidate (làm mới) dữ liệu của trang này
    revalidatePath('/');
  }

  return (
    <div>
      <form action={addTodo}>
        <input type="text" name="todo" />
        <button type="submit">Thêm công việc</button>
      </form>

      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

Khi form được submit, Server Action addTodo không chỉ lưu dữ liệu mà còn gọi revalidatePath('/'). Lệnh này báo cho Next.js rằng dữ liệu tại đường dẫn / (trang chủ) đã cũ và cần được fetch lại. Next.js sẽ tự động chạy lại getTodos, render lại TodoPage với dữ liệu mới nhất và gửi về cho client. Người dùng sẽ thấy công việc mới xuất hiện trong danh sách mà không cần phải tải lại toàn bộ trang.

3. Server Actions vs. API Routes: Khi nào nên dùng cái nào?

Cả Server Actions và API Routes (Route Handlers) đều cho phép client giao tiếp với server. Tuy nhiên, chúng phục vụ cho những mục đích khác nhau và có những ưu/nhược điểm riêng.

Tiêu chíServer ActionsAPI Routes (Route Handlers)
Trường hợp sử dụng chínhCác mutation dữ liệu được kích hoạt bởi người dùng (form submissions, button clicks).Tạo ra các RESTful hoặc GraphQL endpoints có thể tái sử dụng, phục vụ cho nhiều client (web, mobile app, bên thứ ba).
Cách tiếp cậnRPC (Remote Procedure Call) - Gọi hàm trực tiếp.Mô hình Request/Response HTTP truyền thống (GET, POST, PUT, DELETE...).
Độ phức tạpRất đơn giản, không cần boilerplate. Logic có thể đặt cùng component.Cần định nghĩa route, xử lý request/response, method...
Bảo mậtTích hợp sẵn cơ chế chống CSRF. Dữ liệu được mã hóa và giải mã tự động.Phải tự xử lý các vấn đề bảo mật như CSRF, CORS...
Progressive EnhancementHỗ trợ mặc định. Form vẫn hoạt động ngay cả khi JavaScript bị tắt.Phụ thuộc hoàn toàn vào JavaScript phía client.

Lời khuyên:

  • Sử dụng Server Actions cho hầu hết các tác vụ thay đổi dữ liệu bên trong ứng dụng Next.js của bạn. Đây là lựa chọn mặc định và được khuyến khích vì sự đơn giản và tích hợp chặt chẽ.
  • Sử dụng API Routes khi bạn cần xây dựng một API công khai, có cấu trúc rõ ràng để các ứng dụng khác (ví dụ: ứng dụng di động) hoặc các dịch vụ bên thứ ba có thể tiêu thụ.

4. Xử lý trạng thái và lỗi: Nâng cao trải nghiệm người dùng

Trong thực tế, một mutation không phải lúc nào cũng thành công ngay lập tức. Bạn cần xử lý các trạng thái chờ (loading) và các lỗi có thể xảy ra. React hooks như useFormStatususeFormState được sinh ra để giải quyết vấn đề này khi làm việc với Server Actions trong Client Components.

  • useFormStatus: Cung cấp trạng thái pending của form, giúp bạn dễ dàng vô hiệu hóa nút submit hoặc hiển thị một spinner trong khi action đang được xử lý.
  • useFormState: Cho phép Server Action trả về một trạng thái (ví dụ: thông báo lỗi, dữ liệu trả về) và cập nhật nó trong component.

Ví dụ với useFormStatususeFormState:

// app/actions.ts
'use server';

export async function createUser(prevState: any, formData: FormData) {
  const name = formData.get('name');
  if (typeof name !== 'string' || name.length < 3) {
    return { message: 'Tên phải có ít nhất 3 ký tự.' };
  }
  // ... logic tạo user
  revalidatePath('/');
  return { message: 'Tạo người dùng thành công!' };
}
// app/user-form.tsx
'use client'

import { useFormState, useFormStatus } from 'react-dom'
import { createUser } from './actions'

const initialState = {
  message: '',
}

function SubmitButton() {
  const { pending } = useFormStatus()
  return (
    <button type="submit" aria-disabled={pending}>
      {pending ? 'Đang tạo...' : 'Tạo người dùng'}
    </button>
  )
}

export function UserForm() {
  const [state, formAction] = useFormState(createUser, initialState)

  return (
    <form action={formAction}>
      <input type="text" name="name" />
      <SubmitButton />
      <p>{state?.message}</p>
    </form>
  )
}

Trong ví dụ này:

  • UserForm là một Client Component ('use client').
  • useFormState nhận vào Server Action (createUser) và trạng thái ban đầu, trả về trạng thái hiện tại và một phiên bản mới của action để truyền vào form.
  • createUser giờ đây có thể trả về một object chứa thông báo lỗi hoặc thành công.
  • SubmitButton sử dụng useFormStatus để tự động cập nhật giao diện dựa trên trạng thái pending của form.

Kết luận: Server Actions và Mutations, sự thay đổi tư duy

Server Actions và Mutations không chỉ là một tính năng mới; chúng đại diện cho một sự thay đổi trong tư duy về phát triển ứng dụng full-stack với Next.js. Bằng cách xóa bỏ ranh giới giữa client và server, chúng cho phép chúng ta xây dựng các ứng dụng nhanh hơn, ít mã hơn và an toàn hơn.

Việc nắm vững công cụ mạnh mẽ này sẽ mở ra những khả năng mới và giúp bạn tạo ra những trải nghiệm người dùng mượt mà, liền mạch hơn. Hãy bắt đầu tích hợp chúng vào dự án của bạn ngay hôm nay!

Bài viết liên quan

[Next.js Tutorial] Middleware: Hướng dẫn cách dùng với các ví dụ thực tế

Tìm hiểu về Middleware trong Next.js và cách bạn có thể sử dụng chúng để xử lý yêu cầu, điều hướng và xác thực người dùng. Khám phá các ví dụ code thực tế.

[Next.js Tutorial] Dynamic Routes: Hướng dẫn cách dùng và cách tối ưu

Tìm hiểu cách tạo và quản lý Dynamic Routes trong Next.js (App Router). Bài viết hướng dẫn chi tiết từng bước, giúp bạn xây dựng các trang web động hiệu quả và chuẩn SEO.

[Next.js Tutorial] Navigation và Linking: Cách sử dụng cho hiệu suất tối ưu

Bài viết này sẽ giúp bạn làm chủ Navigation và Linking trong Next.js một cách dễ dàng. Tìm hiểu chi tiết cách sử dụng Next/link và useRouter để điều hướng trang, tối ưu hiệu suất ứng dụng của bạn.

[Next.js Tutorial] Triển khai Authentication để tăng cường bảo mật Website

Tìm hiểu cách triển khai Authentication trong Next.js một cách hiệu quả và an toàn. Bài viết này sẽ hướng dẫn bạn từng bước xây dựng hệ thống đăng nhập, đăng ký và bảo vệ các routes riêng tư cho ứng dụng Next.js của bạn.