Next.js 16 Server Actions ปี 2026: Mutation, Revalidation และคำถามสัมภาษณ์

Next.js 16 Server Actions จัดการ mutation, revalidation, สถานะ pending, optimistic UI และความปลอดภัยอย่างไร พร้อมคำถามสัมภาษณ์ที่ทดสอบแต่ละแนวคิด

แผนภาพการไหลของ mutation และ revalidation ใน Next.js Server Actions

Next.js Server Actions เปลี่ยนฟังก์ชัน async ธรรมดาให้กลายเป็น endpoint สำหรับการเปลี่ยนแปลงข้อมูล (mutation) ฝั่งเซิร์ฟเวอร์ที่ฟอร์มเรียกใช้ได้โดยตรง โดยไม่ต้องมี API route ไม่ต้องใช้ fetch ฝั่งไคลเอนต์ และไม่ต้องทำ serialization ของ JSON เอง ใน Next.js 16 นี่คือวิธีมาตรฐานสำหรับการเขียน mutation ของข้อมูล และปรากฏในการสัมภาษณ์งาน React ระดับ senior แทบทุกครั้งในปี 2026 บทวิเคราะห์เชิงลึกนี้อธิบายว่ามันทำงานเบื้องหลังอย่างไร การ revalidation ส่งข้อมูลใหม่ไปยัง UI ได้อย่างไร วิธีจัดการสถานะ pending และ error รวมถึงกฎด้านความปลอดภัยที่ทำให้นักพัฒนาส่วนใหญ่สะดุด

นิยามในหนึ่งประโยค

Server Action คือฟังก์ชัน async ที่กำกับด้วย "use server" ซึ่งทำงานเฉพาะบนเซิร์ฟเวอร์ และเรียกใช้จาก client component หรือจากฟอร์มได้ราวกับเป็นฟังก์ชันภายในเครื่อง ในขณะที่ Next.js จัดการ request ผ่านเครือข่าย การ serialize อาร์กิวเมนต์ และการป้องกัน CSRF ให้โดยอัตโนมัติ

Server Action ทำงานเบื้องหลังอย่างไร

Server Action คือฟังก์ชันที่ทำงานเฉพาะฝั่งเซิร์ฟเวอร์ ซึ่งถูกเรียกผ่านเครือข่ายด้วย endpoint ที่คอมไพเลอร์สร้างขึ้น เมื่อฟังก์ชันมี directive "use server" คอมไพเลอร์ของ Next.js จะไม่ส่งเนื้อหาของฟังก์ชันไปยังเบราว์เซอร์เลย แต่จะแทนที่ฟังก์ชันด้วยการอ้างอิงแบบเบา นั่นคือ action ID ที่ถูกแฮชและแมปไปยัง POST route ที่ถูกสร้างขึ้น เมื่อเรียก action จากไคลเอนต์ ระบบจะส่ง request ไปยัง route นั้น ฟังก์ชันจริงจะทำงานบนเซิร์ฟเวอร์ และผลลัพธ์ที่ผ่านการ serialize จะสตรีมกลับมายังผู้เรียก

การออกแบบนี้มีผลลัพธ์สามประการที่ควรจดจำทั้งสำหรับโค้ดที่ใช้งานจริงและสำหรับการสัมภาษณ์ ประการแรก อาร์กิวเมนต์และค่าที่ส่งคืนต้อง serialize ได้ เพราะมันเดินทางข้ามเครือข่ายในรูปแบบ payload ที่ถูกเข้ารหัส ประการที่สอง ความลับต่าง ๆ ยังคงอยู่บนเซิร์ฟเวอร์ เนื่องจากเนื้อหาของฟังก์ชันไม่เคยถูกรวมเข้าไปใน bundle ของไคลเอนต์ ประการที่สาม action ที่ผูกกับ <form> ทำงานได้แม้ก่อนการ hydration ซึ่งเป็นเหตุผลว่าทำไม progressive enhancement จึงเป็นจุดขายหลัก คู่มือของ Next.js เกี่ยวกับการอัปเดตข้อมูล อธิบายโมเดลการทำงานทั้งหมดไว้อย่างครบถ้วน

Server Action ทุกตัวอยู่ในไฟล์หรือบล็อกที่กำกับด้วย "use server" mutation เดี่ยว ๆ มีหน้าตาเช่นนี้

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}`)
}

ฟังก์ชันนี้อ่านค่า FormData ที่ถูกส่งมา บันทึกข้อมูลผ่าน service ล้างแคชของรายการ และ redirect โดยไม่ต้องใช้ JavaScript ฝั่งไคลเอนต์เลยแม้แต่น้อย

เชื่อม Server Action เข้ากับฟอร์มโดยไม่ต้องใช้ JavaScript ฝั่งไคลเอนต์

การส่ง action ไปยัง prop action ของฟอร์มคือการเชื่อมต่อที่ง่ายที่สุด Next.js จะ serialize ข้อมูลที่ส่งเข้าเป็น FormData และเรียกฟังก์ชันบนเซิร์ฟเวอร์ เนื่องจากวิธีนี้ใช้กลไกการ submit ฟอร์มแบบเนทีฟ มันจึงทำงานได้แม้ปิด JavaScript และยกระดับอย่างราบรื่นเมื่อหน้าเว็บ 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>
  )
}

แอตทริบิวต์ name ของแต่ละฟิลด์จะกลายเป็นคีย์ที่ formData.get() อ่านได้ เอกสารอ้างอิง FormData ของ MDN อธิบายว่าเบราว์เซอร์ประกอบออบเจ็กต์นี้จากฟอร์มอย่างไร หากต้องการควบคุมที่ละเอียดขึ้น next/form ขยายความสามารถของ element แบบเนทีฟด้วยการนำทางฝั่งไคลเอนต์และการ prefetch ในขณะที่ยังคงสัญญาการทำงานแบบ action เดิมไว้

จัดการสถานะ Pending และ Error ด้วย useActionState

ฟอร์มในโลกจริงต้องการทั้งการแสดงผลการตรวจสอบความถูกต้องและตัวบ่งชี้การโหลด hook useActionState ของ React 19 ห่อ Server Action ร้อยค่าสถานะผ่านการ submit แต่ละครั้ง และเปิดเผยแฟล็ก isPending มันมาแทนที่ชื่อเดิม useFormState และคืนค่าเป็น tuple ของสถานะปัจจุบัน action ที่ถูกห่อ และค่าบูลีน 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>
  )
}

เมื่อ hook เป็นตัวขับเคลื่อน action ลายเซ็นของฟังก์ชันจะเพิ่มพารามิเตอร์แรก prevState ไว้ก่อน FormData การคืนค่าเป็นออบเจ็กต์แทนการ throw ช่วยให้คอมโพเนนต์เรนเดอร์การตรวจสอบความถูกต้องแบบ inline ได้โดยไม่ต้องใช้ 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: "" }
}

เอกสารอ้างอิง useActionState บน react.dev ครอบคลุมรูปแบบขั้นสูง เช่น การรองรับ permalink สำหรับการ submit ก่อน hydration การคืนค่าสถานะแบบมีโครงสร้างช่วยให้เส้นทางปกติ (happy path) และเส้นทาง error อยู่ในที่เดียวกันอย่างคาดเดาได้

revalidatePath กับ revalidateTag: เลือกวิธี Invalidation ที่ถูกต้อง

mutation เป็นเพียงครึ่งหนึ่งของงาน UI ต้องสะท้อนการเปลี่ยนแปลงนั้นด้วย Next.js เปิดเผยฟังก์ชัน invalidation สองตัวจาก next/cache และการเลือกตัวที่ถูกต้องคือคำถามที่ใช้แยกแยะผู้สมัครในการสัมภาษณ์บ่อยครั้ง

| ฟังก์ชัน | ล้างแคชอะไร | เหมาะเมื่อ | |----------|-------------|-----------| | revalidatePath("/posts") | ทุกรายการแคชของ route หนึ่ง ๆ | mutation ส่งผลต่อหน้าเดียวหรือ layout ที่ชัดเจน | | revalidateTag("posts") | ทุก fetch ที่แคชและติดป้าย (tag) นั้น | ข้อมูลชุดเดียวกันปรากฏในหลาย route |

revalidatePath เป็นแบบหยาบและยึด route เป็นศูนย์กลาง มันล้างแคชของ URL หนึ่งและ layout tree ของ URL นั้น ส่วน revalidateTag เป็นแบบละเอียดและยึดข้อมูลเป็นศูนย์กลาง fetch หรือฟังก์ชันที่แคชใด ๆ ที่ติดป้ายด้วยสตริงนั้นจะถูกล้างไม่ว่ามันจะอยู่ที่ใด การใช้ tag ขยายตัวได้ดีกว่าในแอปขนาดใหญ่ เพราะ mutation เดียวสามารถรีเฟรช sidebar รายการ และหน้ารายละเอียดได้ในการเรียกเพียงครั้งเดียว ฟังก์ชันทั้งสองทำเครื่องหมายว่าข้อมูล "เก่า" (stale) แทนที่จะ refetch ทันที ดังนั้น request ครั้งถัดไปจึงจะสร้างเนื้อหาขึ้นใหม่

ด้วย Cache Components ใน Next.js 16 การติด tag ทำงานร่วมกับ directive "use cache" ผ่าน cacheTag ซึ่งเปลี่ยนวิธีคิดเกี่ยวกับการแคชทั่วทั้งแอป ปฏิสัมพันธ์นี้อธิบายไว้อย่างละเอียดใน คู่มือ Cache Components ของ Next.js 16 บน SharpSkill

พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

อัปเดต UI แบบ Optimistic ด้วย useOptimistic

การเดินทางไป-กลับผ่านเครือข่ายเพิ่มความหน่วงที่ผู้ใช้รับรู้ได้ hook useOptimistic เรนเดอร์ผลลัพธ์ที่คาดหวังทันที แล้วจึงปรับให้ตรงกับการตอบกลับของเซิร์ฟเวอร์เมื่อ action เสร็จสิ้น มันรับสถานะปัจจุบันและ reducer ที่สร้างเวอร์ชัน optimistic ขึ้นมา

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>
  )
}

หาก action ล้มเหลว React จะย้อนค่า optimistic กลับไปเป็นสถานะที่เซิร์ฟเวอร์ยืนยันโดยอัตโนมัติ จึงไม่จำเป็นต้อง rollback เอง เอกสารอ้างอิง useOptimistic บน react.dev อธิบายรายละเอียดว่าวงจรการปรับให้ตรงกัน (reconciliation) ทำงานร่วมกับ transition อย่างไร รูปแบบนี้คือสิ่งที่ทำให้ฟอร์มแบบ Server Action รู้สึกตอบสนองได้ดีเทียบเท่ากับ SPA ที่เรนเดอร์ฝั่งไคลเอนต์ทั้งหมด

ผูกอาร์กิวเมนต์และเรียก Action จาก Event Handler

ไม่ใช่ทุก mutation ที่มาจากการ submit ฟอร์ม การลบแถว การสลับการตั้งค่า หรือการจัดเรียงรายการใหม่ มักเกิดจากการคลิกปุ่มหรือ event อื่น ๆ Server Action เป็นการอ้างอิงฟังก์ชันปกติบนไคลเอนต์ จึงเรียกใช้จาก handler ใดก็ได้ ตราบใดที่การเรียกนั้นทำงานภายใน transition เพื่อรักษาให้ UI ตอบสนองอยู่เสมอ

อาร์กิวเมนต์เพิ่มเติมที่ไม่ใช่ฟิลด์ของฟอร์มจะถูกแนบด้วย bind ซึ่งสร้าง action ใหม่ที่มีค่าเหล่านั้นนำหน้า นี่คือวิธีที่เป็นสำนวนมาตรฐานในการส่ง ID ควบคู่ไปกับ FormData ที่ถูก submit

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>
  )
}

อาร์กิวเมนต์ที่ผูกไว้จะถูกเข้ารหัสเช่นเดียวกับค่าอื่น ๆ ที่ถูกปิดล้อม (closed-over) ดังนั้น postId จึงไม่ถูกดัดแปลงระหว่างการส่ง แม้ว่า action ยังคงต้องยืนยันว่าผู้เรียกมีสิทธิ์กระทำต่อเรกคอร์ดนั้นก็ตาม การห่อการเรียกโดยตรงไว้ใน startTransition ช่วยให้ React รักษาให้ส่วนติดต่อยังโต้ตอบได้ และแสดงสถานะ pending ออกมาโดยไม่ต้องมีฟอร์มห่อ

ความปลอดภัยของ Server Actions: ปฏิบัติต่อทุก Action เสมือนเป็น API สาธารณะ

ความเข้าใจผิดที่อันตรายที่สุดคือการคิดว่า Server Action เป็นสิ่งที่เป็นส่วนตัวเพราะมันถูกเขียนอยู่ข้าง ๆ โค้ดเซิร์ฟเวอร์ ในความเป็นจริง เมื่อ action ถูกนำไปใช้ Next.js จะสร้าง POST endpoint สาธารณะที่ใครก็ตามสามารถเรียกด้วย request ที่ประดิษฐ์ขึ้นได้ เฟรมเวิร์กจัดหา action ID ที่ปลอดภัยและกำจัด action ที่ไม่ได้ใช้ออกระหว่าง next build แต่มันไม่ได้ตรวจสอบสิทธิ์ของผู้เรียกให้

ตรวจสอบสิทธิ์ภายในทุก Action

Server Actions คือ endpoint HTTP ที่เข้าถึงได้ การตรวจสอบ session ใน layout หรือ middleware ไม่สามารถปกป้องมันได้ เพราะผู้โจมตีสามารถ POST ไปยัง action ได้โดยตรง จงตรวจสอบ session และสิทธิ์ของผู้เรียกภายในตัว action ก่อนที่จะแตะต้องข้อมูลใด ๆ

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)
}

ยังมีอีกสองกฎที่สำคัญ ค่าที่ถูกปิดล้อม (closed-over) โดย action แบบ inline จะถูกเข้ารหัสก่อนส่งไปยังไคลเอนต์และถอดรหัสบนเซิร์ฟเวอร์ ดังนั้นโทเค็นที่ถูกจับไว้จึงไม่รั่วไหลในรูปข้อความธรรมดา แต่การพึ่งพาสิ่งนี้เปราะบาง จงเก็บความลับให้พ้นจาก closure โดยสิ้นเชิง และอาร์กิวเมนต์ใด ๆ ที่มาจากไคลเอนต์คืออินพุตที่ไม่น่าเชื่อถือซึ่งต้องผ่านการตรวจสอบความถูกต้อง เช่นเดียวกับ body ของ REST คู่มือความปลอดภัยของข้อมูลใน Next.js กำหนดโมเดล taint ที่อยู่เบื้องหลังการรับประกันเหล่านี้อย่างเป็นทางการ สำหรับการฝึกฝนอย่างมีโครงสร้างเกี่ยวกับการแลกเปลี่ยนเหล่านี้ โมดูลคำถามสัมภาษณ์ Next.js Server Actions บน SharpSkill เจาะลึกคำถามที่คณะกรรมการสัมภาษณ์ถามจริง

คำถามสัมภาษณ์เรื่อง Server Actions ของ Next.js 16

คำถามเหล่านี้ปรากฏซ้ำ ๆ ในรอบสัมภาษณ์ React และ full-stack ระดับ senior ปี 2026

เหตุใด Server Action จึงรับและคืนค่าได้เฉพาะค่าที่ serialize ได้เท่านั้น เพราะอาร์กิวเมนต์และผลลัพธ์เดินทางข้ามขอบเขตเครือข่ายระหว่างไคลเอนต์กับเซิร์ฟเวอร์ในรูปแบบ payload ที่ถูกเข้ารหัส ฟังก์ชัน อินสแตนซ์ของคลาส และ DOM node ไม่สามารถ serialize ได้ การส่งมันจึงทำให้เกิด error ส่วน FormData ออบเจ็กต์ธรรมดา อาร์เรย์ และค่า primitive นั้นปลอดภัย

progressive enhancement ทำงานร่วมกับ Server Actions อย่างไร เมื่อ action ถูกส่งตรงไปยัง prop action ของฟอร์ม เบราว์เซอร์จะ submit ฟอร์มแบบเนทีฟผ่าน request แบบ POST แม้ก่อนที่ React จะ hydrate Next.js จะดักจับและรัน action ฝั่งเซิร์ฟเวอร์ ดังนั้นฟอร์มจึงทำงานได้โดยไม่ต้องใช้ JavaScript ฝั่งไคลเอนต์ และยกระดับเป็นประสบการณ์ที่ขับเคลื่อนด้วยไคลเอนต์หลังการ hydration

ความแตกต่างระหว่างการ throw กับการคืนค่า error ใน action คืออะไร การ throw จะส่งต่อไปยัง error boundary ที่ใกล้ที่สุด และเหมาะกับความล้มเหลวที่ไม่คาดคิด ส่วนการคืนค่าออบเจ็กต์สถานะที่มีโครงสร้างผ่าน useActionState เหมาะกับ error การตรวจสอบความถูกต้องที่คาดหมายได้ ซึ่งควรแสดงผลแบบ inline โดยไม่ต้อง unmount ฟอร์ม

การเรียก revalidatePath refetch ข้อมูลทันทีหรือไม่ ไม่ มันทำเครื่องหมายรายการแคชว่าเก่า (stale) เพื่อให้ request ครั้งถัดไปสร้างมันขึ้นใหม่ การตอบกลับปัจจุบันไม่ได้รับผลกระทบ ซึ่งเป็นเหตุผลว่าทำไมการ redirect หลัง mutation จึงไปตกที่เนื้อหาที่เพิ่งเรนเดอร์ใหม่

สำหรับบริบทการเรนเดอร์ที่กว้างขึ้นซึ่ง mutation เหล่านี้ทำงานอยู่ภายใน บทวิเคราะห์ React 19 Server Components ในการใช้งานจริง เข้าคู่กันอย่างเป็นธรรมชาติกับความรู้เรื่อง Server Actions

พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

บทสรุป

Server Actions ของ Next.js 16 รวมการทำ mutation การ revalidation และ progressive enhancement เข้าไว้ในไพรมิทีฟที่ยึดเซิร์ฟเวอร์เป็นหลักเพียงหนึ่งเดียว ประเด็นสำคัญมีดังนี้

  • กำกับ mutation ด้วย "use server" และส่งมันไปยัง prop action ของฟอร์ม เพื่อให้ได้การจัดการเครือข่ายและ progressive enhancement มาฟรี ๆ
  • ขับเคลื่อนสถานะ pending และการตรวจสอบความถูกต้องด้วย useActionState โดยคืนค่าออบเจ็กต์ที่มีโครงสร้างสำหรับ error ที่คาดหมายได้แทนการ throw
  • เลือกใช้ revalidatePath เมื่อมี route เดียวเปลี่ยนแปลง และ revalidateTag เมื่อข้อมูลชุดเดียวกันครอบคลุมหลาย route
  • ใช้ useOptimistic เพื่อเรนเดอร์ผลลัพธ์ที่คาดหวังทันที และปล่อยให้ React ปรับให้ตรงกันหรือย้อนกลับโดยอัตโนมัติ
  • ตรวจสอบสิทธิ์และความถูกต้องภายในตัว action ทุกตัว เพราะ action ที่ถูกใช้แต่ละตัวคือ endpoint HTTP สาธารณะที่ middleware เพียงอย่างเดียวไม่สามารถปกป้องได้

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

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

แชร์

บทความที่เกี่ยวข้อง