คู่มือฉบับสมบูรณ์เกี่ยวกับ Next.js 14 Server Actions ครอบคลุมแนวทางปฏิบัติที่ดีที่สุดในการจัดการฟอร์ม การตรวจสอบข้อมูล ข้อควรพิจารณาด้านความปลอดภัย และเทคนิคขั้นสูงสำหรับการสร้างเว็บแอปพลิเคชันสมัยใหม่
Next.js 14 Server Actions: แนวทางปฏิบัติที่ดีที่สุดสำหรับการจัดการฟอร์มอย่างมืออาชีพ
Next.js 14 มาพร้อมกับฟีเจอร์อันทรงพลังสำหรับการสร้างเว็บแอปพลิเคชันที่มีประสิทธิภาพและใช้งานง่าย หนึ่งในนั้นคือ Server Actions ซึ่งโดดเด่นในฐานะวิธีการปฏิวัติการจัดการการส่งฟอร์มและการเปลี่ยนแปลงข้อมูลโดยตรงบนเซิร์ฟเวอร์ คู่มือนี้จะให้ภาพรวมที่ครอบคลุมเกี่ยวกับ Server Actions ใน Next.js 14 โดยเน้นที่แนวทางปฏิบัติที่ดีที่สุดสำหรับการจัดการฟอร์ม การตรวจสอบข้อมูล ความปลอดภัย และเทคนิคขั้นสูง เราจะสำรวจตัวอย่างที่ใช้งานได้จริงและให้ข้อมูลเชิงลึกที่นำไปปฏิบัติได้เพื่อช่วยให้คุณสร้างเว็บแอปพลิเคชันที่แข็งแกร่งและขยายขนาดได้
Next.js Server Actions คืออะไร?
Server Actions คือฟังก์ชันแบบอะซิงโครนัสที่ทำงานบนเซิร์ฟเวอร์และสามารถเรียกใช้ได้โดยตรงจากคอมโพเนนต์ของ React มันช่วยลดความจำเป็นในการสร้าง API routes แบบดั้งเดิมสำหรับการจัดการการส่งฟอร์มและการเปลี่ยนแปลงข้อมูล ส่งผลให้โค้ดเรียบง่ายขึ้น ความปลอดภัยดีขึ้น และประสิทธิภาพสูงขึ้น Server Actions เป็น React Server Components (RSCs) ซึ่งหมายความว่ามันจะถูกประมวลผลบนเซิร์ฟเวอร์ ทำให้การโหลดหน้าเว็บครั้งแรกเร็วขึ้นและดีต่อ SEO
ประโยชน์หลักของ Server Actions:
- โค้ดที่เรียบง่ายขึ้น: ลดโค้ดที่ต้องเขียนซ้ำซ้อนโดยไม่จำเป็นต้องสร้าง API routes แยกต่างหาก
- ความปลอดภัยที่ดีขึ้น: การประมวลผลฝั่งเซิร์ฟเวอร์ช่วยลดช่องโหว่ฝั่งไคลเอ็นต์
- ประสิทธิภาพที่เพิ่มขึ้น: ประมวลผลการเปลี่ยนแปลงข้อมูลโดยตรงบนเซิร์ฟเวอร์เพื่อเวลาตอบสนองที่รวดเร็วยิ่งขึ้น
- SEO ที่ปรับให้เหมาะสม: ใช้ประโยชน์จาก server-side rendering เพื่อการจัดทำดัชนีของเครื่องมือค้นหาที่ดีขึ้น
- ความปลอดภัยของประเภทข้อมูล (Type Safety): ได้รับประโยชน์จากความปลอดภัยของประเภทข้อมูลแบบ end-to-end ด้วย TypeScript
การตั้งค่าโปรเจกต์ Next.js 14 ของคุณ
ก่อนที่จะลงลึกในเรื่อง Server Actions ตรวจสอบให้แน่ใจว่าคุณได้ตั้งค่าโปรเจกต์ Next.js 14 เรียบร้อยแล้ว หากคุณเริ่มต้นจากศูนย์ ให้สร้างโปรเจกต์ใหม่โดยใช้คำสั่งต่อไปนี้:
npx create-next-app@latest my-next-app
ตรวจสอบให้แน่ใจว่าโปรเจกต์ของคุณใช้โครงสร้างไดเรกทอรี app
เพื่อใช้ประโยชน์จาก Server Components และ Actions ได้อย่างเต็มที่
การจัดการฟอร์มเบื้องต้นด้วย Server Actions
เรามาเริ่มด้วยตัวอย่างง่ายๆ: ฟอร์มที่ส่งข้อมูลเพื่อสร้างรายการใหม่ในฐานข้อมูล เราจะใช้ฟอร์มง่ายๆ ที่มีช่องป้อนข้อมูลและปุ่มส่ง
ตัวอย่าง: การสร้างรายการใหม่
ขั้นแรก กำหนดฟังก์ชัน Server Action ภายในคอมโพเนนต์ React ของคุณ ฟังก์ชันนี้จะจัดการตรรกะการส่งฟอร์มบนเซิร์ฟเวอร์
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
// จำลองการทำงานกับฐานข้อมูล
console.log('Creating item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // จำลองความหน่วงแฝง
console.log('Item created successfully!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
await createItem(formData);
setIsSubmitting(false);
}
return (
);
}
คำอธิบาย:
- directive
'use client'
บ่งชี้ว่านี่คือ client component - ฟังก์ชัน
createItem
ถูกทำเครื่องหมายด้วย directive'use server'
ซึ่งบ่งชี้ว่าเป็น Server Action - ฟังก์ชัน
handleSubmit
เป็นฟังก์ชันฝั่งไคลเอ็นต์ที่เรียกใช้ server action และยังจัดการสถานะของ UI เช่น การปิดใช้งานปุ่มระหว่างการส่งข้อมูล - prop
action
ขององค์ประกอบ<form>
ถูกตั้งค่าเป็นฟังก์ชันhandleSubmit
- เมธอด
formData.get('name')
ดึงค่าของช่องป้อนข้อมูล 'name' await new Promise
จำลองการทำงานของฐานข้อมูลและเพิ่มความหน่วงแฝง
การตรวจสอบข้อมูล (Data Validation)
การตรวจสอบข้อมูลเป็นสิ่งสำคัญอย่างยิ่งในการรับรองความสมบูรณ์ของข้อมูลและป้องกันช่องโหว่ด้านความปลอดภัย Server Actions เป็นโอกาสที่ดีเยี่ยมในการตรวจสอบข้อมูลฝั่งเซิร์ฟเวอร์ แนวทางนี้ช่วยลดความเสี่ยงที่เกี่ยวข้องกับการตรวจสอบฝั่งไคลเอ็นต์เพียงอย่างเดียว
ตัวอย่าง: การตรวจสอบข้อมูลอินพุต
แก้ไข Server Action createItem
เพื่อรวมตรรกะการตรวจสอบข้อมูลเข้าไป
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
if (!name || name.length < 3) {
throw new Error('ชื่อรายการต้องมีความยาวอย่างน้อย 3 ตัวอักษร');
}
// จำลองการทำงานกับฐานข้อมูล
console.log('Creating item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // จำลองความหน่วงแฝง
console.log('Item created successfully!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'เกิดข้อผิดพลาด');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
คำอธิบาย:
- ฟังก์ชัน
createItem
ตอนนี้จะตรวจสอบว่าname
ถูกต้องหรือไม่ (มีความยาวอย่างน้อย 3 ตัวอักษร) - หากการตรวจสอบล้มเหลว จะมีการโยนข้อผิดพลาด (throw an error)
- ฟังก์ชัน
handleSubmit
ได้รับการอัปเดตเพื่อดักจับข้อผิดพลาดใดๆ ที่โยนมาจาก Server Action และแสดงข้อความแสดงข้อผิดพลาดแก่ผู้ใช้
การใช้ไลบรารีสำหรับการตรวจสอบข้อมูล
สำหรับสถานการณ์การตรวจสอบข้อมูลที่ซับซ้อนยิ่งขึ้น ลองพิจารณาใช้ไลบรารีสำหรับการตรวจสอบข้อมูล เช่น:
- Zod: ไลบรารีการประกาศสคีมาและการตรวจสอบข้อมูลที่เน้น TypeScript เป็นหลัก
- Yup: ตัวสร้างสคีมาของ JavaScript สำหรับการแยกวิเคราะห์ ตรวจสอบ และแปลงค่า
นี่คือตัวอย่างการใช้ Zod:
// app/utils/validation.ts
import { z } from 'zod';
export const CreateItemSchema = z.object({
name: z.string().min(3, 'ชื่อรายการต้องมีความยาวอย่างน้อย 3 ตัวอักษร'),
});
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
import { CreateItemSchema } from '../utils/validation';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
const validatedFields = CreateItemSchema.safeParse({ name });
if (!validatedFields.success) {
return { errors: validatedFields.error.flatten().fieldErrors };
}
// จำลองการทำงานกับฐานข้อมูล
console.log('Creating item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // จำลองความหน่วงแฝง
console.log('Item created successfully!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'เกิดข้อผิดพลาด');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
คำอธิบาย:
CreateItemSchema
กำหนดกฎการตรวจสอบข้อมูลสำหรับฟิลด์name
โดยใช้ Zod- เมธอด
safeParse
พยายามตรวจสอบข้อมูลอินพุต หากการตรวจสอบล้มเหลว จะส่งคืนอ็อบเจกต์พร้อมข้อผิดพลาด - อ็อบเจกต์
errors
มีข้อมูลโดยละเอียดเกี่ยวกับความล้มเหลวในการตรวจสอบ
ข้อควรพิจารณาด้านความปลอดภัย
Server Actions ช่วยเพิ่มความปลอดภัยโดยการรันโค้ดบนเซิร์ฟเวอร์ แต่ก็ยังคงเป็นสิ่งสำคัญที่จะต้องปฏิบัติตามแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุดเพื่อปกป้องแอปพลิเคชันของคุณจากภัยคุกคามทั่วไป
การป้องกัน Cross-Site Request Forgery (CSRF)
การโจมตีแบบ CSRF ใช้ประโยชน์จากความไว้วางใจที่เว็บไซต์มีต่อเบราว์เซอร์ของผู้ใช้ เพื่อป้องกันการโจมตีแบบ CSRF ให้ใช้กลไกการป้องกัน CSRF
Next.js จะจัดการการป้องกัน CSRF โดยอัตโนมัติเมื่อใช้ Server Actions เฟรมเวิร์กจะสร้างและตรวจสอบโทเค็น CSRF สำหรับการส่งฟอร์มแต่ละครั้ง เพื่อให้แน่ใจว่าคำขอมาจากแอปพลิเคชันของคุณ
การจัดการการยืนยันตัวตนและการให้สิทธิ์ผู้ใช้
ตรวจสอบให้แน่ใจว่ามีเพียงผู้ใช้ที่ได้รับอนุญาตเท่านั้นที่สามารถดำเนินการบางอย่างได้ ใช้กลไกการยืนยันตัวตนและการให้สิทธิ์เพื่อปกป้องข้อมูลและฟังก์ชันที่ละเอียดอ่อน
นี่คือตัวอย่างการใช้ NextAuth.js เพื่อป้องกัน Server Action:
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
import { getServerSession } from 'next-auth';
import { authOptions } from '../../app/api/auth/[...nextauth]/route';
async function createItem(formData: FormData) {
'use server'
const session = await getServerSession(authOptions);
if (!session) {
throw new Error('ไม่ได้รับอนุญาต');
}
const name = formData.get('name') as string;
// จำลองการทำงานกับฐานข้อมูล
console.log('Creating item:', name, 'by user:', session.user?.email);
await new Promise((resolve) => setTimeout(resolve, 1000)); // จำลองความหน่วงแฝง
console.log('Item created successfully!');
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'เกิดข้อผิดพลาด');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
คำอธิบาย:
- ฟังก์ชัน
getServerSession
ดึงข้อมูลเซสชันของผู้ใช้ - หากผู้ใช้ไม่ได้รับการยืนยันตัวตน (ไม่มีเซสชัน) จะมีการโยนข้อผิดพลาด เพื่อป้องกันไม่ให้ Server Action ทำงาน
การกรองข้อมูลอินพุต (Sanitizing Input Data)
กรองข้อมูลอินพุตเพื่อป้องกันการโจมตีแบบ Cross-Site Scripting (XSS) การโจมตีแบบ XSS เกิดขึ้นเมื่อโค้ดที่เป็นอันตรายถูกฉีดเข้าไปในเว็บไซต์ ซึ่งอาจเป็นอันตรายต่อข้อมูลผู้ใช้หรือการทำงานของแอปพลิเคชัน
ใช้ไลบรารีเช่น DOMPurify
หรือ sanitize-html
เพื่อกรองข้อมูลที่ผู้ใช้ป้อนเข้ามาก่อนที่จะประมวลผลใน Server Actions ของคุณ
เทคนิคขั้นสูง
เมื่อเราได้ครอบคลุมพื้นฐานแล้ว มาสำรวจเทคนิคขั้นสูงบางอย่างสำหรับการใช้ Server Actions อย่างมีประสิทธิภาพกัน
การอัปเดตเชิงบวก (Optimistic Updates)
การอัปเดตเชิงบวกมอบประสบการณ์ผู้ใช้ที่ดีขึ้นโดยการอัปเดต UI ทันทีเสมือนว่าการกระทำนั้นจะสำเร็จ แม้กระทั่งก่อนที่เซิร์ฟเวอร์จะยืนยันก็ตาม หากการกระทำล้มเหลวบนเซิร์ฟเวอร์ UI จะถูกเปลี่ยนกลับไปสู่สถานะก่อนหน้า
// app/components/UpdateItemForm.tsx
'use client';
import { useState } from 'react';
async function updateItem(id: string, formData: FormData) {
'use server'
const name = formData.get('name') as string;
// จำลองการทำงานกับฐานข้อมูล
console.log('Updating item:', id, 'with name:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // จำลองความหน่วงแฝง
// จำลองความล้มเหลว (เพื่อการสาธิต)
const shouldFail = Math.random() < 0.5;
if (shouldFail) {
throw new Error('ไม่สามารถอัปเดตรายการได้');
}
console.log('Item updated successfully!');
return { name }; // ส่งคืนชื่อที่อัปเดตแล้ว
}
export default function UpdateItemForm({ id, initialName }: { id: string; initialName: string }) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [itemName, setItemName] = useState(initialName);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
// อัปเดต UI ในเชิงบวก
const newName = formData.get('name') as string;
setItemName(newName);
try {
const result = await updateItem(id, formData);
//หากสำเร็จ การอัปเดตจะแสดงผลใน UI ผ่าน setItemName แล้ว
} catch (error: any) {
setErrorMessage(error.message || 'เกิดข้อผิดพลาด');
// ย้อนกลับ UI เมื่อเกิดข้อผิดพลาด
setItemName(initialName);
} finally {
setIsSubmitting(false);
}
}
return (
ชื่อปัจจุบัน: {itemName}
{errorMessage && {errorMessage}
}
);
}
คำอธิบาย:
- ก่อนที่จะเรียกใช้ Server Action, UI จะถูกอัปเดตทันทีด้วยชื่อรายการใหม่โดยใช้
setItemName
- หาก Server Action ล้มเหลว UI จะถูกเปลี่ยนกลับไปเป็นชื่อรายการเดิม
การตรวจสอบข้อมูลใหม่ (Revalidating Data)
หลังจากที่ Server Action แก้ไขข้อมูล คุณอาจต้องทำการ revalidate ข้อมูลที่แคชไว้เพื่อให้แน่ใจว่า UI แสดงการเปลี่ยนแปลงล่าสุด Next.js มีหลายวิธีในการ revalidate ข้อมูล:
- Revalidate Path: Revalidate แคชสำหรับเส้นทาง (path) ที่ระบุ
- Revalidate Tag: Revalidate แคชสำหรับข้อมูลที่เชื่อมโยงกับแท็ก (tag) ที่ระบุ
นี่คือตัวอย่างของการ revalidate path หลังจากสร้างรายการใหม่:
// app/components/CreateItemForm.tsx
'use client';
import { useState } from 'react';
import { revalidatePath } from 'next/cache';
async function createItem(formData: FormData) {
'use server'
const name = formData.get('name') as string;
// จำลองการทำงานกับฐานข้อมูล
console.log('Creating item:', name);
await new Promise((resolve) => setTimeout(resolve, 1000)); // จำลองความหน่วงแฝง
console.log('Item created successfully!');
revalidatePath('/items'); // Revalidate path /items
}
export default function CreateItemForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
setErrorMessage(null);
try {
await createItem(formData);
} catch (error: any) {
setErrorMessage(error.message || 'เกิดข้อผิดพลาด');
} finally {
setIsSubmitting(false);
}
}
return (
{errorMessage && {errorMessage}
}
);
}
คำอธิบาย:
- ฟังก์ชัน
revalidatePath('/items')
ทำให้แคชสำหรับ path/items
ไม่ถูกต้อง เพื่อให้แน่ใจว่าคำขอถัดไปสำหรับ path นั้นจะดึงข้อมูลล่าสุด
แนวทางปฏิบัติที่ดีที่สุดสำหรับ Server Actions
เพื่อเพิ่มประโยชน์สูงสุดจาก Server Actions ให้พิจารณาแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้:
- ทำให้ Server Actions มีขนาดเล็กและมุ่งเน้น: Server Actions ควรทำงานเพียงอย่างเดียวที่กำหนดไว้อย่างชัดเจน หลีกเลี่ยงตรรกะที่ซับซ้อนภายใน Server Actions เพื่อรักษาความสามารถในการอ่านและการทดสอบ
- ใช้ชื่อที่สื่อความหมาย: ตั้งชื่อ Server Actions ของคุณให้สื่อความหมายและบ่งบอกวัตถุประสงค์อย่างชัดเจน
- จัดการข้อผิดพลาดอย่างเหมาะสม: ใช้การจัดการข้อผิดพลาดที่แข็งแกร่งเพื่อให้ข้อเสนอแนะที่เป็นประโยชน์แก่ผู้ใช้และป้องกันแอปพลิเคชันล่ม
- ตรวจสอบข้อมูลอย่างละเอียด: ทำการตรวจสอบข้อมูลอย่างครอบคลุมเพื่อรับรองความสมบูรณ์ของข้อมูลและป้องกันช่องโหว่ด้านความปลอดภัย
- รักษาความปลอดภัยของ Server Actions ของคุณ: ใช้กลไกการยืนยันตัวตนและการให้สิทธิ์เพื่อปกป้องข้อมูลและฟังก์ชันที่ละเอียดอ่อน
- ปรับปรุงประสิทธิภาพ: ตรวจสอบประสิทธิภาพของ Server Actions ของคุณและปรับปรุงตามความจำเป็นเพื่อให้แน่ใจว่ามีเวลาตอบสนองที่รวดเร็ว
- ใช้การแคชอย่างมีประสิทธิภาพ: ใช้ประโยชน์จากกลไกการแคชของ Next.js เพื่อปรับปรุงประสิทธิภาพและลดภาระของฐานข้อมูล
ข้อผิดพลาดที่พบบ่อยและวิธีหลีกเลี่ยง
แม้ว่า Server Actions จะมีข้อดีมากมาย แต่ก็มีข้อผิดพลาดทั่วไปบางประการที่ควรระวัง:
- Server Actions ที่ซับซ้อนเกินไป: หลีกเลี่ยงการใส่ตรรกะมากเกินไปใน Server Action เดียว แบ่งงานที่ซับซ้อนออกเป็นฟังก์ชันที่เล็กและจัดการได้ง่ายกว่า
- ละเลยการจัดการข้อผิดพลาด: รวมการจัดการข้อผิดพลาดไว้เสมอเพื่อดักจับข้อผิดพลาดที่ไม่คาดคิดและให้ข้อเสนอแนะที่เป็นประโยชน์แก่ผู้ใช้
- เพิกเฉยต่อแนวทางปฏิบัติด้านความปลอดภัย: ปฏิบัติตามแนวทางปฏิบัติด้านความปลอดภัยเพื่อปกป้องแอปพลิเคชันของคุณจากภัยคุกคามทั่วไป เช่น XSS และ CSRF
- ลืม revalidate ข้อมูล: ตรวจสอบให้แน่ใจว่าคุณได้ revalidate ข้อมูลที่แคชไว้หลังจากที่ Server Action แก้ไขข้อมูลเพื่อให้ UI อัปเดตอยู่เสมอ
สรุป
Next.js 14 Server Actions เป็นวิธีที่ทรงพลังและมีประสิทธิภาพในการจัดการการส่งฟอร์มและการเปลี่ยนแปลงข้อมูลโดยตรงบนเซิร์ฟเวอร์ โดยการปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดที่ระบุไว้ในคู่มือนี้ คุณสามารถสร้างเว็บแอปพลิเคชันที่แข็งแกร่ง ปลอดภัย และมีประสิทธิภาพได้ ลองใช้ Server Actions เพื่อทำให้โค้ดของคุณง่ายขึ้น เพิ่มความปลอดภัย และปรับปรุงประสบการณ์ผู้ใช้โดยรวม ในขณะที่คุณนำหลักการเหล่านี้ไปใช้ ให้พิจารณาถึงผลกระทบในระดับโลกจากตัวเลือกการพัฒนาของคุณ ตรวจสอบให้แน่ใจว่าฟอร์มและกระบวนการจัดการข้อมูลของคุณสามารถเข้าถึงได้ ปลอดภัย และเป็นมิตรกับผู้ใช้สำหรับผู้ชมจากนานาชาติที่หลากหลาย ความมุ่งมั่นในการสร้างความเท่าเทียมนี้ไม่เพียงแต่จะปรับปรุงการใช้งานของแอปพลิเคชันของคุณ แต่ยังขยายการเข้าถึงและประสิทธิผลในระดับโลกอีกด้วย