บทนำ: ฟอร์มใน Next.js ยุค 2026 เปลี่ยนไปอย่างไร
ถ้าคุณเคยสร้างฟอร์มใน React แบบเดิมๆ คุณน่าจะคุ้นเคยกับขั้นตอนที่ค่อนข้างยุ่งยากดี — สร้าง API route แยก, จัดการ state ด้วย useState, fetch ด้วย POST, จัดการ loading state เอง แล้วยังต้องจัดการ error อีก ทุกอย่างต้องเขียนเองหมดเลย
ตรงๆ เลยนะ มันเหนื่อย
แต่ในปี 2026 ด้วย Next.js 16 และ React 19 ภาพรวมของการสร้างฟอร์มเปลี่ยนไปค่อนข้างเยอะ Server Actions ช่วยให้เราเรียกฟังก์ชันบน server จาก component ได้โดยตรง ไม่ต้องสร้าง API endpoint ด้วยตัวเอง ส่วน useActionState จาก React 19 จะจัดการ state ของฟอร์มทั้ง pending, error และ success ให้อัตโนมัติ และเมื่อรวมกับ Zod สำหรับ validation เราก็จะได้ระบบฟอร์มที่ปลอดภัย, type-safe และรองรับ Progressive Enhancement ตั้งแต่ต้น
ในคู่มือนี้ เราจะมาสร้างฟอร์มกันตั้งแต่เริ่มต้นจนถึงขั้นสูง พร้อมโค้ดตัวอย่างที่เอาไปใช้ได้จริงทุกขั้นตอนเลย
Server Actions คืออะไร
Server Actions คือฟังก์ชัน async ที่ทำงานบน server แต่เราสามารถเรียกใช้จาก React component ได้โดยตรงเลย เหมือนเรียกฟังก์ชัน JavaScript ธรรมดาๆ นี่แหละ เบื้องหลัง Next.js จะสร้าง POST endpoint ให้อัตโนมัติ ไม่ต้องมานั่งสร้าง Route Handler เอง
การประกาศ Server Action ทำได้ 2 วิธี:
วิธีที่ 1: ประกาศในไฟล์แยก (แนะนำ)
// app/actions/contact.ts
'use server'
export async function submitContact(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
// บันทึกลงฐานข้อมูล
await db.contact.create({
data: { name, email, message }
})
}
วิธีที่ 2: ประกาศ inline ใน Server Component
// app/contact/page.tsx
export default function ContactPage() {
async function handleSubmit(formData: FormData) {
'use server'
const name = formData.get('name') as string
// ทำงานบน server...
}
return (
<form action={handleSubmit}>
<input name="name" />
<button type="submit">ส่ง</button>
</form>
)
}
หลักการสำคัญ: Server Actions ออกแบบมาสำหรับ การเปลี่ยนแปลงข้อมูล (mutations) เช่น สร้าง, อัพเดท, ลบ ไม่ใช่สำหรับดึงข้อมูลนะ สำหรับ data fetching ควรใช้ Server Components หรือ Route Handlers แทน จุดนี้คนมักจะสับสนกันบ่อยพอสมควร
useActionState คืออะไร
useActionState เป็น hook ใหม่จาก React 19 ที่มาแทนที่ useFormState ตัวเก่า ทำหน้าที่จัดการ state ที่เกิดจากการ submit ฟอร์ม — ไม่ว่าจะเป็น pending state, error state หรือ success state ก็จัดการให้หมด
hook นี้ return ค่า 3 ตัว:
- state — ค่า state ปัจจุบัน (ครั้งแรกจะเป็น initialState ที่เรากำหนด)
- formAction — ฟังก์ชันที่ส่งเข้า
actionprop ของ<form> - isPending — boolean บอกว่าฟอร์มกำลังส่งอยู่หรือเปล่า
'use client'
import { useActionState } from 'react'
const [state, formAction, isPending] = useActionState(
serverAction,
initialState
)
ข้อดีของ useActionState ที่ชอบมากคือเราไม่ต้องจัดการ loading state ด้วย useState เอง ไม่ต้อง try/catch ใน client ไม่ต้อง fetch เอง hook มันจัดการให้ครบ ประหยัดโค้ดไปได้เยอะเลย
เริ่มต้นสร้างโปรเจกต์
มาตั้งค่าโปรเจกต์และติดตั้ง dependencies ที่จำเป็นกันก่อนเลย:
npx create-next-app@latest my-form-app --typescript --app
cd my-form-app
npm install zod
โปรเจกต์นี้ใช้ Next.js 16 กับ React 19 ซึ่ง useActionState พร้อมใช้งานเลย ไม่ต้องติดตั้งอะไรเพิ่ม
สร้างฟอร์ม Contact แบบเต็มรูปแบบ
ถึงเวลาลงมือจริงแล้ว เราจะสร้างฟอร์มติดต่อ (Contact Form) ที่มีฟีเจอร์ครบถ้วน: validation ด้วย Zod, จัดการ state ด้วย useActionState, แสดง error แบบ field-level และรองรับ Progressive Enhancement ด้วย
ขั้นตอนที่ 1: สร้าง Zod Schema
// lib/schemas/contact.ts
import { z } from 'zod'
export const contactSchema = z.object({
name: z
.string()
.min(2, { message: 'ชื่อต้องมีอย่างน้อย 2 ตัวอักษร' })
.max(100, { message: 'ชื่อยาวเกินไป' }),
email: z
.string()
.email({ message: 'รูปแบบอีเมลไม่ถูกต้อง' }),
subject: z
.string()
.min(5, { message: 'หัวข้อต้องมีอย่างน้อย 5 ตัวอักษร' })
.max(200, { message: 'หัวข้อยาวเกินไป' }),
message: z
.string()
.min(10, { message: 'ข้อความต้องมีอย่างน้อย 10 ตัวอักษร' })
.max(5000, { message: 'ข้อความยาวเกินไป' }),
})
export type ContactFormData = z.infer<typeof contactSchema>
เราแยก schema ออกมาเป็นไฟล์ต่างหาก เพราะจะได้ใช้ schema เดียวกันนี้ทั้ง client-side validation และ server-side validation เขียนทีเดียวใช้ได้ทั้งสองฝั่ง สะดวกมาก
ขั้นตอนที่ 2: สร้าง Server Action
// app/actions/contact.ts
'use server'
import { contactSchema } from '@/lib/schemas/contact'
// กำหนด type สำหรับ state ที่ return กลับ
export type ContactFormState = {
errors?: {
name?: string[]
email?: string[]
subject?: string[]
message?: string[]
}
message?: string
success?: boolean
}
export async function submitContactForm(
prevState: ContactFormState,
formData: FormData
): Promise<ContactFormState> {
// แปลง FormData เป็น object
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
subject: formData.get('subject'),
message: formData.get('message'),
}
// Validate ด้วย Zod
const validatedFields = contactSchema.safeParse(rawData)
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'กรุณาตรวจสอบข้อมูลที่กรอก',
success: false,
}
}
try {
// บันทึกลงฐานข้อมูล
await db.contact.create({
data: validatedFields.data,
})
return {
message: 'ส่งข้อความสำเร็จแล้ว ขอบคุณที่ติดต่อเรา!',
success: true,
}
} catch (error) {
return {
message: 'เกิดข้อผิดพลาด กรุณาลองใหม่อีกครั้ง',
success: false,
}
}
}
จุดที่ต้องระวัง: parameter แรกของ Server Action ที่ใช้กับ useActionState จะเป็น prevState เสมอ ไม่ใช่ formData เหมือน Server Action ปกติ ตรงนี้แหละที่หลายคนสะดุด (รวมถึงตัวผมเองตอนเริ่มใช้ครั้งแรกด้วย)
ขั้นตอนที่ 3: สร้าง Form Component
// app/contact/contact-form.tsx
'use client'
import { useActionState } from 'react'
import { submitContactForm, type ContactFormState } from '@/app/actions/contact'
const initialState: ContactFormState = {
errors: {},
message: '',
success: false,
}
export function ContactForm() {
const [state, formAction, isPending] = useActionState(
submitContactForm,
initialState
)
return (
<form action={formAction} className="space-y-4">
{/* แสดงข้อความสำเร็จ */}
{state.success && (
<div className="p-4 bg-green-50 text-green-800 rounded-lg">
{state.message}
</div>
)}
{/* แสดงข้อความ error ระดับฟอร์ม */}
{state.message && !state.success && (
<div className="p-4 bg-red-50 text-red-800 rounded-lg">
{state.message}
</div>
)}
{/* ฟิลด์ชื่อ */}
<div>
<label htmlFor="name">ชื่อ</label>
<input
id="name"
name="name"
type="text"
required
aria-describedby="name-error"
className="w-full border rounded-lg p-2"
/>
{state.errors?.name && (
<p id="name-error" className="text-red-600 text-sm mt-1">
{state.errors.name[0]}
</p>
)}
</div>
{/* ฟิลด์อีเมล */}
<div>
<label htmlFor="email">อีเมล</label>
<input
id="email"
name="email"
type="email"
required
aria-describedby="email-error"
className="w-full border rounded-lg p-2"
/>
{state.errors?.email && (
<p id="email-error" className="text-red-600 text-sm mt-1">
{state.errors.email[0]}
</p>
)}
</div>
{/* ฟิลด์หัวข้อ */}
<div>
<label htmlFor="subject">หัวข้อ</label>
<input
id="subject"
name="subject"
type="text"
required
aria-describedby="subject-error"
className="w-full border rounded-lg p-2"
/>
{state.errors?.subject && (
<p id="subject-error" className="text-red-600 text-sm mt-1">
{state.errors.subject[0]}
</p>
)}
</div>
{/* ฟิลด์ข้อความ */}
<div>
<label htmlFor="message">ข้อความ</label>
<textarea
id="message"
name="message"
rows={5}
required
aria-describedby="message-error"
className="w-full border rounded-lg p-2"
/>
{state.errors?.message && (
<p id="message-error" className="text-red-600 text-sm mt-1">
{state.errors.message[0]}
</p>
)}
</div>
{/* ปุ่ม submit */}
<button
type="submit"
disabled={isPending}
className="bg-blue-600 text-white px-6 py-2 rounded-lg
disabled:opacity-50 disabled:cursor-not-allowed"
>
{isPending ? 'กำลังส่ง...' : 'ส่งข้อความ'}
</button>
</form>
)
}
ขั้นตอนที่ 4: สร้างหน้า Page
// app/contact/page.tsx
import { ContactForm } from './contact-form'
export default function ContactPage() {
return (
<main className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">ติดต่อเรา</h1>
<ContactForm />
</main>
)
}
การ Validate ทั้ง Client-side และ Server-side
จุดนี้สำคัญมากเลยนะ: Client-side validation มีไว้เพื่อ UX ส่วน server-side validation มีไว้เพื่อ security ต้อง validate ทั้งสองฝั่งเสมอ เพราะ client-side validation ถูก bypass ได้ง่ายมากผ่าน DevTools หรือส่ง request ตรงๆ
ข้อดีของการใช้ Zod schema ร่วมกันก็คือ เราเขียน schema ครั้งเดียวแล้วใช้ได้ทั้งสองฝั่งเลย:
// app/contact/contact-form-with-client-validation.tsx
'use client'
import { useActionState, useRef, useState } from 'react'
import { submitContactForm, type ContactFormState } from '@/app/actions/contact'
import { contactSchema } from '@/lib/schemas/contact'
const initialState: ContactFormState = {
errors: {},
message: '',
success: false,
}
export function ContactFormWithValidation() {
const [state, formAction, isPending] = useActionState(
submitContactForm,
initialState
)
const [clientErrors, setClientErrors] = useState<Record<string, string>>({})
const formRef = useRef<HTMLFormElement>(null)
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
const formData = new FormData(e.currentTarget)
const rawData = Object.fromEntries(formData)
// Validate ฝั่ง client ก่อน
const result = contactSchema.safeParse(rawData)
if (!result.success) {
e.preventDefault() // หยุด submit ถ้า validate ไม่ผ่าน
const errors: Record<string, string> = {}
result.error.issues.forEach((issue) => {
const field = issue.path[0] as string
if (!errors[field]) {
errors[field] = issue.message
}
})
setClientErrors(errors)
return
}
setClientErrors({}) // เคลียร์ error ก่อน submit
// ปล่อยให้ form submit ต่อไปยัง Server Action
}
return (
<form
ref={formRef}
action={formAction}
onSubmit={handleSubmit}
className="space-y-4"
noValidate
>
{/* ใช้ clientErrors สำหรับ instant feedback */}
{/* ใช้ state.errors สำหรับ server-side errors */}
<div>
<label htmlFor="name">ชื่อ</label>
<input id="name" name="name" type="text" />
{(clientErrors.name || state.errors?.name?.[0]) && (
<p className="text-red-600 text-sm">
{clientErrors.name || state.errors?.name?.[0]}
</p>
)}
</div>
{/* ... ฟิลด์อื่นๆ ในรูปแบบเดียวกัน */}
<button type="submit" disabled={isPending}>
{isPending ? 'กำลังส่ง...' : 'ส่ง'}
</button>
</form>
)
}
สิ่งที่เกิดขึ้นคือ เมื่อผู้ใช้กดส่ง ฟอร์มจะ validate ฝั่ง client ก่อน ถ้าไม่ผ่านก็แสดง error ทันทีโดยไม่ต้องส่งไป server เลย แต่ถ้าผ่านก็จะส่งไป Server Action ซึ่งจะ validate อีกครั้งฝั่ง server เพื่อความปลอดภัย ลองคิดดูว่าแค่นี้ก็จัดการได้ทั้ง UX และ security แล้ว
useFormStatus: แสดง Loading State แยก Component
ถ้าต้องการแยก submit button ออกเป็น component ต่างหาก (เช่น เอาไปใช้ร่วมกับหลายฟอร์ม) ให้ใช้ useFormStatus จาก react-dom แทน
ข้อจำกัดที่ต้องจำไว้: useFormStatus ต้องอยู่ใน component ลูกของ <form> เท่านั้น ใช้ใน component เดียวกับที่ประกาศ <form> ไม่ได้ ตรงนี้ถ้าลืมจะงงเลยว่าทำไม pending ไม่ทำงาน
// components/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton({ label = 'ส่ง' }: { label?: string }) {
const { pending } = useFormStatus()
return (
<button
type="submit"
disabled={pending}
className="bg-blue-600 text-white px-6 py-2 rounded-lg
disabled:opacity-50"
>
{pending ? (
<span className="flex items-center gap-2">
<span className="animate-spin">⏳</span>
กำลังดำเนินการ...
</span>
) : (
label
)}
</button>
)
}
// ใช้งานใน form
<form action={formAction}>
{/* ... input fields ... */}
<SubmitButton label="ส่งข้อความ" />
</form>
Progressive Enhancement: ฟอร์มทำงานแม้ไม่มี JavaScript
นี่คือหนึ่งในข้อดีที่ชอบมากที่สุดของ Server Actions เลย — Progressive Enhancement ฟอร์มของเราทำงานได้แม้ว่า JavaScript ยังโหลดไม่เสร็จ หรือแม้แต่ถูกปิดไว้ก็ตาม
เมื่อใช้ action prop ของ <form> กับ Server Action:
- ถ้า JavaScript ยังโหลดไม่เสร็จ — ฟอร์มจะส่งแบบ HTML form ปกติ
- ถ้า JavaScript โหลดเสร็จแล้ว — React จะ intercept การ submit แล้วส่งผ่าน fetch API แทน ได้ประสบการณ์ที่ดีกว่าเยอะ (ไม่ต้อง reload หน้า)
มาดูวิธีทำให้ Progressive Enhancement ทำงานได้ดีกัน:
// Server Component — ไม่ต้อง 'use client'
export default function SimpleFeedbackPage() {
async function submitFeedback(formData: FormData) {
'use server'
const feedback = formData.get('feedback') as string
await db.feedback.create({ data: { content: feedback } })
}
return (
<form action={submitFeedback}>
<textarea name="feedback" required />
<button type="submit">ส่ง Feedback</button>
</form>
)
}
ฟอร์มนี้ทำงานได้ 100% แม้ไม่มี JavaScript เลย เพราะเป็น Server Component ที่ใช้ HTML form action ดั้งเดิม ง่ายและเชื่อถือได้
การ Revalidate ข้อมูลหลังจาก Submit
หลังจากที่ฟอร์ม submit สำเร็จ เราอาจต้องการอัพเดท UI ให้แสดงข้อมูลใหม่ด้วย ใน Next.js 16 มี API หลายตัวให้จัดการเรื่องนี้:
ใช้ revalidatePath
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(
prevState: FormState,
formData: FormData
) {
// ... validate และบันทึกข้อมูล
// revalidate หน้ารายการโพสต์
revalidatePath('/posts')
return { success: true, message: 'สร้างโพสต์สำเร็จ' }
}
ใช้ redirect หลัง mutation
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(
prevState: FormState,
formData: FormData
) {
// ... validate และบันทึกข้อมูล
revalidatePath('/posts')
redirect('/posts') // redirect ไปหน้ารายการ
}
ใช้ updateTag ใน Next.js 16 (ใหม่)
ใน Next.js 16 มี API ใหม่ชื่อ updateTag ที่ออกแบบมาสำหรับ Server Actions โดยเฉพาะ จะ expire cache ทันทีเพื่อให้ผู้ใช้เห็นข้อมูลที่ตัวเองเพิ่งเปลี่ยนแปลง (แนวคิด read-your-own-writes):
'use server'
import { updateTag } from 'next/cache'
export async function updateProfile(
prevState: FormState,
formData: FormData
) {
// ... validate และอัพเดทข้อมูล
// expire cache ทันที — ผู้ใช้จะเห็นข้อมูลใหม่เลย
updateTag('user-profile')
return { success: true, message: 'อัพเดทโปรไฟล์สำเร็จ' }
}
รูปแบบขั้นสูง: ฟอร์มที่ไม่ reset เมื่อ Error
นี่คือปัญหาที่เจอบ่อยมากจริงๆ: เมื่อใช้ uncontrolled inputs กับ action prop ใน React 19 ฟอร์มจะถูก reset หลัง submit ทุกครั้ง แม้ว่าจะเกิด error ก็ตาม ผลคือผู้ใช้ต้องกรอกข้อมูลใหม่ทั้งหมด ซึ่งน่าหงุดหงิดมาก
วิธีแก้ก็ไม่ยาก — return ข้อมูลที่ผู้ใช้กรอกกลับมาพร้อมกับ error state:
// app/actions/contact.ts
'use server'
export type ContactFormState = {
errors?: Record<string, string[]>
message?: string
success?: boolean
fieldValues?: {
name: string
email: string
subject: string
message: string
}
}
export async function submitContactForm(
prevState: ContactFormState,
formData: FormData
): Promise<ContactFormState> {
const rawData = {
name: formData.get('name') as string,
email: formData.get('email') as string,
subject: formData.get('subject') as string,
message: formData.get('message') as string,
}
const result = contactSchema.safeParse(rawData)
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
message: 'กรุณาตรวจสอบข้อมูล',
success: false,
fieldValues: rawData, // ส่งข้อมูลกลับมาด้วย!
}
}
// ... บันทึกข้อมูล
return { success: true, message: 'สำเร็จ!' }
}
// ใน form component
<input
id="name"
name="name"
type="text"
defaultValue={state.fieldValues?.name || ''}
/>
ด้วยวิธีนี้ เมื่อ validation ไม่ผ่าน ข้อมูลที่ผู้ใช้กรอกไว้จะยังคงอยู่ในฟอร์ม แก้แค่จุดที่ผิดแล้วส่งใหม่ได้เลย UX ดีขึ้นเยอะมาก
ความปลอดภัย: สิ่งที่ต้องรู้
Server Actions อาจดูเหมือนปลอดภัยเพราะทำงานบน server แต่จริงๆ แล้วมันคือ public HTTP endpoint ที่ใครก็ส่ง request มาได้ อย่าลืมปฏิบัติกับมันเหมือนกับ API endpoint ทั่วไป:
- Validate ข้อมูลทุกครั้งบน server — ไม่ว่าจะมี client-side validation แล้วก็ตาม
- ตรวจสอบ authentication — เช็คว่าผู้ใช้ล็อกอินแล้วก่อนทำ mutation
- ตรวจสอบ authorization — เช็คว่าผู้ใช้มีสิทธิ์ทำ action นั้นจริงๆ
- อย่าไว้ใจ client-side validation — มันมีไว้เพื่อ UX เท่านั้น (ย้ำอีกรอบ!)
'use server'
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
export async function deletePost(
prevState: FormState,
formData: FormData
) {
// ตรวจสอบ authentication
const session = await auth()
if (!session?.user) {
redirect('/login')
}
const postId = formData.get('postId') as string
// ตรวจสอบ authorization
const post = await db.post.findUnique({ where: { id: postId } })
if (post?.authorId !== session.user.id) {
return { success: false, message: 'คุณไม่มีสิทธิ์ลบโพสต์นี้' }
}
await db.post.delete({ where: { id: postId } })
revalidatePath('/posts')
return { success: true, message: 'ลบโพสต์สำเร็จ' }
}
ข่าวดีคือ Next.js มีระบบ CSRF protection ในตัวอยู่แล้ว: Server Actions รับเฉพาะ POST method และใช้ SameSite cookies ทำให้ปลอดภัยจากการโจมตี CSRF ส่วนใหญ่ นอกจากนี้ action ID ที่สร้างขึ้นจะถูก encrypt และเปลี่ยนทุกครั้งที่ build ใหม่ด้วย
สรุปรูปแบบที่แนะนำ
จากที่เราผ่านมาทั้งหมด นี่คือสรุปรูปแบบที่แนะนำสำหรับการสร้างฟอร์มด้วย Server Actions:
- ฟอร์มง่ายๆ — ใช้ Server Action ใน Server Component โดยตรง รองรับ Progressive Enhancement เต็มที่
- ฟอร์มที่ต้องการ error handling — ใช้
useActionStateใน Client Component พร้อม Zod validation บน server - ฟอร์มที่ต้องการ instant feedback — เพิ่ม client-side validation ด้วย Zod schema เดียวกัน
- Loading indicator — ใช้
isPendingจากuseActionStateหรือuseFormStatusใน component ลูก - หลัง mutation — ใช้
revalidatePath,redirectหรือupdateTag(Next.js 16) ตามกรณี
ส่วนตัวคิดว่าสำหรับฟอร์มส่วนใหญ่ในโปรเจกต์จริง แค่ใช้ useActionState + Zod validation บน server ก็เพียงพอแล้ว เพิ่ม client-side validation ทีหลังถ้ารู้สึกว่า UX ยังไม่ดีพอก็ได้
คำถามที่พบบ่อย (FAQ)
useActionState กับ useFormState ต่างกันอย่างไร?
useFormState เป็น API เดิมที่ถูกเปลี่ยนชื่อเป็น useActionState ใน React 19 ทั้งสองทำงานเหมือนกัน แต่ useActionState มีข้อดีเพิ่มคือ return ค่า isPending เป็นตัวที่สาม ทำให้ไม่ต้องใช้ useFormStatus แยกอีกในหลายกรณี ถ้าใช้ React 19 ขึ้นไป ให้ใช้ useActionState อย่างเดียวเลย
Server Actions ใช้ดึงข้อมูล (data fetching) ได้ไหม?
ในทางเทคนิคทำได้ แต่ ไม่แนะนำ เพราะ Server Actions ใช้ POST request ซึ่งออกแบบมาสำหรับ mutations โดยเฉพาะ สำหรับการดึงข้อมูลควรใช้ Server Components หรือ Route Handlers จะเหมาะกว่า
จำเป็นต้องใช้ Zod ไหม?
ไม่จำเป็น ใช้ library อื่นอย่าง Yup, Valibot หรือแม้แต่เขียน validation เองก็ได้ สิ่งที่สำคัญคือต้อง validate ข้อมูลบน server เสมอ ไม่ว่าจะใช้เครื่องมือใด แต่เหตุผลที่ Zod เป็นที่นิยมก็เพราะมัน type-safe, schema ใช้ร่วมกันได้ทั้ง client/server และ error message อ่านง่าย ซึ่งสำหรับโปรเจกต์ Next.js + TypeScript มันลงตัวมากจริงๆ
ฟอร์มจะทำงานได้ไหมถ้า JavaScript ยังโหลดไม่เสร็จ?
ได้เลย ถ้าใช้ action prop ของ <form> กับ Server Action ฟอร์มจะทำงานแบบ HTML form ปกติเมื่อ JavaScript ยังไม่พร้อม แต่ฟีเจอร์อย่าง useActionState จะไม่ทำงานจนกว่า JavaScript จะโหลดเสร็จนะ ดังนั้นถ้าต้องการ Progressive Enhancement เต็มรูปแบบจริงๆ ให้ใช้ Server Action ใน Server Component โดยตรงจะดีที่สุด
updateTag ใน Next.js 16 ต่างจาก revalidateTag อย่างไร?
updateTag เป็น API ใหม่ใน Next.js 16 ที่ใช้ได้เฉพาะใน Server Actions มันจะ expire cache ทันทีเพื่อให้ผู้ใช้เห็นข้อมูลที่เพิ่งเปลี่ยนแปลงได้เลย ส่วน revalidateTag ใช้ได้ทั้งใน Server Actions และ Route Handlers รองรับ stale-while-revalidate (ส่งข้อมูลเก่าก่อนแล้วอัพเดทเบื้องหลัง) กฎง่ายๆ คือ ใช้ updateTag เมื่อผู้ใช้ต้องเห็นข้อมูลใหม่ทันที ใช้ revalidateTag เมื่อยอมรับความล่าช้าเล็กน้อยได้