Server Actions trong Next.js 16 (2026): Mutation, Revalidation và câu hỏi phỏng vấn

Cách Server Actions trong Next.js 16 xử lý mutation, revalidation, trạng thái đang chờ, giao diện lạc quan và bảo mật, kèm những câu hỏi phỏng vấn kiểm tra từng khái niệm.

Sơ đồ luồng mutation và revalidation của Server Actions trong Next.js

Server Actions của Next.js biến một hàm async thông thường thành một endpoint xử lý mutation phía máy chủ mà form có thể gọi trực tiếp, không cần API route, không cần fetch phía client, và không cần tự tay serialize JSON. Trong Next.js 16, đây là cách mặc định để viết các thao tác thay đổi dữ liệu, và chúng xuất hiện trong hầu hết mọi buổi phỏng vấn React cấp senior năm 2026. Bài phân tích chuyên sâu này mổ xẻ cách chúng thực thi, cách quá trình revalidation truyền dữ liệu mới tới giao diện, cách mô hình hóa trạng thái đang chờ và lỗi, cùng những quy tắc bảo mật khiến phần lớn lập trình viên vấp phải.

Định nghĩa trong một câu

Server Action là một hàm async được đánh dấu bằng "use server", chỉ chạy trên máy chủ và có thể được gọi từ một client component hoặc một form như thể nó là hàm cục bộ, trong khi Next.js tự động xử lý request mạng, việc serialize tham số, và bảo vệ CSRF.

Cách Server Actions thực thi bên dưới lớp vỏ

Một Server Action là một hàm chỉ chạy trên máy chủ, được gọi qua mạng thông qua một endpoint do trình biên dịch sinh ra. Khi một hàm mang chỉ thị "use server", trình biên dịch của Next.js không bao giờ gửi phần thân hàm xuống trình duyệt. Thay vào đó, nó thay thế hàm bằng một tham chiếu nhẹ: một action ID đã băm được ánh xạ tới một route POST được sinh tự động. Khi gọi action từ client, một request sẽ được gửi tới route đó, hàm thật chạy trên máy chủ, và kết quả đã được serialize sẽ được stream ngược lại phía gọi.

Thiết kế này kéo theo ba hệ quả đáng ghi nhớ cho cả code sản xuất lẫn phỏng vấn. Thứ nhất, tham số và giá trị trả về phải serialize được, vì chúng vượt qua mạng dưới dạng payload đã mã hóa. Thứ hai, các bí mật vẫn nằm trên máy chủ, bởi phần thân hàm không bao giờ được đóng gói cho client. Thứ ba, các action gắn vào một <form> vẫn hoạt động ngay cả trước khi hydrate, đó là lý do progressive enhancement là một điểm bán hàng cốt lõi. Hướng dẫn cập nhật dữ liệu của Next.js mô tả đầy đủ mô hình thực thi này.

Mọi Server Action đều nằm trong một tệp hoặc một khối được đánh dấu "use server". Một mutation đơn giản trông như sau:

app/posts/actions.tstypescript
"use server"

import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import { PostService } from "@/lib/services/post-service"

export async function createPost(formData: FormData) {
  // FormData values arrive as strings; cast explicitly
  const title = formData.get("title") as string
  const body = formData.get("body") as string

  // Run business logic through the service layer, never the ORM directly
  const post = await PostService.create({ title, body })

  // Purge the cached list so the next request refetches it
  revalidatePath("/posts")

  // Send the user to the freshly created resource
  redirect(`/posts/${post.id}`)
}

Hàm đọc FormData đã gửi lên, lưu dữ liệu thông qua một service, vô hiệu hóa cache của danh sách, rồi chuyển hướng. Không cần bất kỳ JavaScript phía client nào cho toàn bộ quá trình này.

Kết nối Server Action với form mà không cần JavaScript phía client

Truyền action vào prop action của form là cách tích hợp đơn giản nhất. Next.js serialize dữ liệu gửi đi thành FormData và gọi hàm trên máy chủ. Vì cách này sử dụng cơ chế submit form gốc của trình duyệt, nó vẫn hoạt động khi JavaScript bị tắt và nâng cấp mượt mà ngay khi trang được hydrate.

app/posts/new-post-form.tsxtsx
import { createPost } from "./actions"

export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">Publish</button>
    </form>
  )
}

Thuộc tính name trên mỗi trường trở thành khóa mà formData.get() đọc. Tài liệu tham khảo FormData của MDN giải thích cách trình duyệt lắp ráp đối tượng này từ form. Để kiểm soát tinh vi hơn, next/form mở rộng phần tử gốc với điều hướng phía client và prefetch trong khi vẫn giữ nguyên hợp đồng dựa trên action.

Quản lý trạng thái đang chờ và lỗi với useActionState

Các form thực tế cần phản hồi kiểm tra hợp lệ và một chỉ báo tải. Hook useActionState của React 19 bao bọc một Server Action, luồn một giá trị trạng thái qua mỗi lần submit, và phơi bày một cờ isPending. Nó thay thế tên gọi cũ useFormState và trả về một tuple gồm trạng thái hiện tại, một action đã được bao bọc, và giá trị boolean pending.

app/posts/new-post-form.tsxtsx
"use client"

import { useActionState } from "react"
import { createPost } from "./actions"

const initialState = { error: "" }

export default function NewPostForm() {
  // state carries the last return value; isPending tracks the in-flight request
  const [state, formAction, isPending] = useActionState(createPost, initialState)

  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="body" required />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button type="submit" disabled={isPending}>
        {isPending ? "Publishing..." : "Publish"}
      </button>
    </form>
  )
}

Khi một hook điều khiển action, chữ ký hàm sẽ có thêm tham số đầu tiên prevState đứng trước FormData. Việc trả về một đối tượng thay vì ném lỗi cho phép component hiển thị kiểm tra hợp lệ ngay tại chỗ mà không cần error boundary:

app/posts/actions.tstypescript
"use server"

import { revalidateTag } from "next/cache"
import { PostService } from "@/lib/services/post-service"

type FormState = { error: string }

export async function createPost(
  prevState: FormState,
  formData: FormData,
): Promise<FormState> {
  const title = formData.get("title") as string

  // Validate on the server; client validation is never trustworthy alone
  if (!title || title.length < 3) {
    return { error: "Title must be at least 3 characters." }
  }

  await PostService.create({ title })

  // Invalidate by tag rather than path for finer-grained control
  revalidateTag("posts")
  return { error: "" }
}

Tài liệu tham khảo useActionState trên react.dev trình bày các mẫu nâng cao như hỗ trợ permalink cho những lần submit trước hydrate. Việc trả về trạng thái có cấu trúc giữ cho luồng thành công và luồng lỗi nằm ở cùng một nơi dễ đoán.

revalidatePath so với revalidateTag: chọn đúng cách vô hiệu hóa cache

Một mutation mới chỉ hoàn thành một nửa công việc; giao diện phải phản ánh nó. Next.js cung cấp hai hàm vô hiệu hóa từ next/cache, và việc chọn đúng hàm là một điểm phân loại thường gặp trong phỏng vấn.

| Hàm | Vô hiệu hóa | Phù hợp nhất khi | |----------|-------------|-----------| | revalidatePath("/posts") | Mọi entry cache của một route | Mutation ảnh hưởng tới một trang hoặc layout rõ ràng | | revalidateTag("posts") | Mọi fetch được cache gắn nhãn đó | Cùng một dữ liệu xuất hiện trên nhiều route |

revalidatePath mang tính thô và lấy route làm trung tâm: nó xóa cache cho một URL và cây layout của nó. revalidateTag mang tính chi tiết và lấy dữ liệu làm trung tâm: bất kỳ fetch hay hàm được cache nào gắn nhãn chuỗi đó đều bị xóa ở mọi nơi nó tồn tại. Tag mở rộng tốt hơn trong các ứng dụng lớn vì một mutation có thể làm mới một sidebar, một danh sách, và một trang chi tiết chỉ trong một lời gọi. Cả hai hàm đều đánh dấu dữ liệu là cũ thay vì fetch lại ngay lập tức, nên request kế tiếp sẽ tái tạo nội dung.

Với Cache Components của Next.js 16, việc gắn tag tích hợp với chỉ thị "use cache" thông qua cacheTag, làm thay đổi cách suy luận về caching trên toàn ứng dụng. Tương tác đó được trình bày chuyên sâu trong hướng dẫn Cache Components của Next.js 16 trên SharpSkill.

Sẵn sàng chinh phục phỏng vấn React / Next.js?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Cập nhật giao diện lạc quan với useOptimistic

Các vòng round-trip qua mạng thêm độ trễ mà người dùng cảm nhận được. Hook useOptimistic hiển thị kết quả kỳ vọng ngay lập tức, rồi hòa hợp với phản hồi từ máy chủ khi action hoàn tất. Nó nhận trạng thái hiện tại và một reducer sinh ra phiên bản lạc quan.

app/posts/like-button.tsxtsx
"use client"

import { useOptimistic } from "react"
import { likePost } from "./actions"

export function LikeButton({ postId, likes }: { postId: string; likes: number }) {
  // optimisticLikes updates before the server confirms
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (current) => current + 1,
  )

  async function handleLike() {
    addOptimisticLike(null) // update the UI immediately
    await likePost(postId)  // then run the real mutation
  }

  return (
    <form action={handleLike}>
      <button type="submit">Like ({optimisticLikes})</button>
    </form>
  )
}

Nếu action thất bại, React tự động khôi phục giá trị lạc quan về trạng thái đã được máy chủ xác nhận, nên không cần rollback thủ công. Tài liệu tham khảo useOptimistic trên react.dev trình bày chi tiết cách chu trình hòa hợp tương tác với transition. Chính mẫu này khiến các form dùng Server Action mang lại cảm giác phản hồi nhanh như một SPA render hoàn toàn phía client.

Gắn tham số và gọi Action từ trình xử lý sự kiện

Không phải mọi mutation đều bắt nguồn từ việc submit form. Việc xóa một hàng, bật hoặc tắt một cài đặt, hay sắp xếp lại một danh sách thường được kích hoạt từ một cú click nút hoặc một sự kiện khác. Một Server Action là một tham chiếu hàm bình thường trên client, nên nó có thể được gọi từ bất kỳ trình xử lý nào, miễn là lời gọi chạy bên trong một transition để giữ giao diện phản hồi tốt.

Các tham số bổ sung không phải trường của form được đính kèm bằng bind, tạo ra một action mới với các giá trị đó được thêm vào đầu. Đây là cách idiomatic để truyền một ID cùng với FormData đã gửi.

app/posts/post-actions.tsxtsx
"use client"

import { useTransition } from "react"
import { deletePost, updatePost } from "./actions"

export function PostActions({ postId }: { postId: string }) {
  const [isPending, startTransition] = useTransition()

  // Bind the id so the action receives it before FormData
  const updateWithId = updatePost.bind(null, postId)

  return (
    <div>
      <form action={updateWithId}>
        <input name="title" />
        <button type="submit">Save</button>
      </form>

      {/* Event-handler invocation wrapped in a transition */}
      <button
        onClick={() => startTransition(() => deletePost(postId))}
        disabled={isPending}
      >
        {isPending ? "Deleting..." : "Delete"}
      </button>
    </div>
  )
}

Các tham số được bind được mã hóa như mọi giá trị được đóng gói khác, nên postId không bị can thiệp khi truyền đi, dù vậy action vẫn phải xác nhận rằng người gọi được phép tác động lên bản ghi đó. Việc bao bọc lời gọi trực tiếp trong startTransition cho phép React giữ giao diện luôn tương tác được và làm lộ ra trạng thái pending mà không cần bọc trong form.

Bảo mật Server Actions: coi mọi Action như một API công khai

Quan niệm sai lầm nguy hiểm nhất là cho rằng một Server Action là riêng tư vì nó được viết cạnh code máy chủ. Trên thực tế, một khi một action được sử dụng, Next.js sinh ra một endpoint POST công khai mà bất kỳ ai cũng có thể gọi bằng một request được dựng thủ công. Framework cung cấp các action ID an toàn và loại bỏ các action không dùng đến trong quá trình next build, nhưng nó không cấp quyền cho người gọi.

Cấp quyền bên trong mọi Action

Server Actions là các endpoint HTTP có thể truy cập được. Việc kiểm tra session trong một layout hay middleware không bảo vệ chúng, vì kẻ tấn công có thể POST thẳng tới action. Hãy xác minh session và quyền của người gọi bên trong phần thân action trước khi chạm vào bất kỳ dữ liệu nào.

app/admin/actions.tstypescript
"use server"

import { auth } from "@/lib/auth"
import { headers } from "next/headers"
import { UserService } from "@/lib/services/user-service"

export async function deleteUser(userId: string) {
  // Authenticate on every invocation, not in a wrapper component
  const session = await auth.api.getSession({ headers: await headers() })
  if (!session || session.user.role !== "admin") {
    throw new Error("Unauthorized")
  }

  await UserService.delete(userId)
}

Hai quy tắc nữa cũng quan trọng. Các giá trị được đóng gói bởi một action nội tuyến sẽ được mã hóa trước khi gửi tới client và giải mã trên máy chủ, nên một token bị đóng gói không bị rò rỉ dưới dạng văn bản thuần, nhưng dựa vào điều đó là mong manh: hãy giữ các bí mật hoàn toàn nằm ngoài closure. Và bất kỳ tham số nào đến từ client đều là dữ liệu đầu vào không đáng tin, phải được kiểm tra hợp lệ, hệt như một body của REST. Hướng dẫn bảo mật dữ liệu của Next.js chính thức hóa mô hình taint đứng sau các bảo đảm này. Để luyện tập có cấu trúc về những đánh đổi này, module câu hỏi phỏng vấn Next.js Server Actions trên SharpSkill rèn đúng những câu hỏi mà các hội đồng tuyển dụng hay hỏi.

Câu hỏi phỏng vấn về Server Actions của Next.js 16

Những câu hỏi này liên tục xuất hiện trong các vòng phỏng vấn React và full-stack cấp senior năm 2026.

Vì sao một Server Action chỉ có thể nhận và trả về các giá trị serialize được? Vì tham số và kết quả vượt qua ranh giới mạng giữa client và máy chủ dưới dạng payload đã mã hóa. Các hàm, thể hiện của class, và nút DOM không thể serialize, nên truyền chúng sẽ ném lỗi. FormData, các đối tượng thuần, mảng, và các kiểu nguyên thủy thì an toàn.

Progressive enhancement hoạt động thế nào với Server Actions? Khi một action được truyền trực tiếp vào prop action của form, trình duyệt submit form theo cách gốc qua một request POST ngay cả trước khi React hydrate. Next.js chặn lại và chạy action phía máy chủ, nên form vẫn hoạt động mà không cần JavaScript phía client và nâng cấp thành trải nghiệm điều khiển bởi client sau khi hydrate.

Đâu là khác biệt giữa việc ném lỗi và trả về lỗi trong một action? Ném lỗi sẽ lan tới error boundary gần nhất và phù hợp với các thất bại bất ngờ. Trả về một đối tượng trạng thái có cấu trúc qua useActionState phù hợp với các lỗi kiểm tra hợp lệ được lường trước, cần hiển thị ngay tại chỗ mà không unmount form.

Gọi revalidatePath có fetch lại dữ liệu ngay lập tức không? Không. Nó đánh dấu các entry cache là cũ để request kế tiếp tái tạo chúng. Phản hồi hiện tại không bị ảnh hưởng, đó là lý do một redirect sau mutation đáp xuống nội dung vừa được render mới.

Để hiểu bối cảnh render rộng hơn mà các mutation này chạy bên trong, bài phân tích React 19 Server Components trong sản xuất kết hợp tự nhiên với kiến thức về Server Actions.

Sẵn sàng chinh phục phỏng vấn React / Next.js?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Kết luận

Server Actions của Next.js 16 hợp nhất mutation, revalidation, và progressive enhancement thành một primitive ưu tiên máy chủ duy nhất. Những điểm chính rút ra:

  • Đánh dấu các mutation bằng "use server" và truyền chúng vào prop action của form để nhận việc xử lý mạng và progressive enhancement một cách miễn phí.
  • Điều khiển trạng thái đang chờ và kiểm tra hợp lệ bằng useActionState, trả về một đối tượng có cấu trúc cho các lỗi được lường trước thay vì ném lỗi.
  • Dùng revalidatePath khi một route thay đổi và revalidateTag khi cùng một dữ liệu trải trên nhiều route.
  • Dùng useOptimistic để hiển thị kết quả kỳ vọng ngay lập tức và để React tự hòa hợp hoặc rollback.
  • Cấp quyền và kiểm tra hợp lệ bên trong phần thân của mọi action, vì mỗi action được dùng là một endpoint HTTP công khai mà chỉ riêng middleware không bảo vệ được.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#next.js
#server-actions
#react
#mutations
#revalidation
#interview

Chia sẻ

Bài viết liên quan