ไทย

ปลดล็อกพลังของ hook useActionState ใน React เรียนรู้วิธีจัดการฟอร์มให้ง่ายขึ้น จัดการสถานะ pending และปรับปรุงประสบการณ์ผู้ใช้ด้วยตัวอย่างที่ลึกซึ้งและใช้งานได้จริง

React useActionState: คู่มือฉบับสมบูรณ์สำหรับการจัดการฟอร์มสมัยใหม่

โลกของการพัฒนาเว็บมีการเปลี่ยนแปลงอยู่ตลอดเวลา และระบบนิเวศของ React ก็อยู่แถวหน้าของการเปลี่ยนแปลงนี้ ด้วยเวอร์ชันล่าสุด React ได้เปิดตัวคุณสมบัติอันทรงพลังที่ปรับปรุงวิธีการสร้างแอปพลิเคชันเชิงโต้ตอบและยืดหยุ่นได้อย่างถึงแก่น หนึ่งในสิ่งที่ส่งผลกระทบมากที่สุดคือ hook useActionState ซึ่งเป็นตัวเปลี่ยนเกมสำหรับการจัดการฟอร์มและการดำเนินการแบบอะซิงโครนัส hook นี้ ซึ่งก่อนหน้านี้รู้จักกันในชื่อ useFormState ในเวอร์ชันทดลอง บัดนี้ได้กลายเป็นเครื่องมือที่เสถียรและจำเป็นสำหรับนักพัฒนา React สมัยใหม่ทุกคน

คู่มือฉบับสมบูรณ์นี้จะพาคุณเจาะลึก useActionState เราจะสำรวจปัญหาที่มันแก้ไข กลไกหลัก และวิธีการใช้งานร่วมกับ hook อื่นๆ เช่น useFormStatus เพื่อสร้างประสบการณ์ผู้ใช้ที่เหนือกว่า ไม่ว่าคุณจะสร้างฟอร์มติดต่อง่ายๆ หรือแอปพลิเคชันที่ซับซ้อนและใช้ข้อมูลจำนวนมาก การทำความเข้าใจ useActionState จะทำให้โค้ดของคุณสะอาดขึ้น เป็นแบบ declarative มากขึ้น และแข็งแกร่งขึ้น

ปัญหา: ความซับซ้อนของการจัดการ State ของฟอร์มแบบดั้งเดิม

ก่อนที่เราจะเข้าใจความยอดเยี่ยมของ useActionState เราต้องเข้าใจถึงความท้าทายที่มันเข้ามาแก้ไขก่อน เป็นเวลาหลายปีที่การจัดการ state ของฟอร์มใน React เกี่ยวข้องกับรูปแบบที่คาดเดาได้ แต่ก็มักจะยุ่งยากโดยใช้ hook useState

ลองพิจารณาสถานการณ์ทั่วไป: ฟอร์มง่ายๆ สำหรับเพิ่มผลิตภัณฑ์ใหม่ลงในรายการ เราต้องจัดการ state หลายส่วน:

การนำไปใช้งานโดยทั่วไปอาจมีลักษณะดังนี้:

ตัวอย่าง: 'วิธีเก่า' ด้วย useState หลายตัว

// ฟังก์ชัน API สมมติ
const addProductAPI = async (productName) => {
await new Promise(resolve => setTimeout(resolve, 1500));
if (!productName || productName.length < 3) {
throw new Error('Product name must be at least 3 characters long.');
}
console.log(`Product "${productName}" added.`);
return { success: true };
};

// คอมโพเนนต์
import { useState } from 'react';

function OldProductForm() {
const [productName, setProductName] = useState('');
const [error, setError] = useState(null);
const [isPending, setIsPending] = useState(false);

const handleSubmit = async (event) => {
event.preventDefault();
setIsPending(true);
setError(null);

try {
await addProductAPI(productName);
setProductName(''); // ล้างอินพุตเมื่อสำเร็จ
} catch (err) {
setError(err.message);
} finally {
setIsPending(false);
}
};

return (




id="productName"
name="productName"
value={productName}
onChange={(e) => setProductName(e.target.value)}
/>

{error &&

{error}

}


);
}

แนวทางนี้ใช้งานได้ แต่ก็มีข้อเสียหลายประการ:

  • Boilerplate: เราต้องเรียก useState ถึงสามครั้งแยกกันเพื่อจัดการสิ่งที่ในทางแนวคิดแล้วเป็นกระบวนการส่งฟอร์มเพียงกระบวนการเดียว
  • การจัดการ State ด้วยตนเอง: นักพัฒนาต้องรับผิดชอบในการตั้งค่าและรีเซ็ตสถานะ loading และ error ด้วยตนเองตามลำดับที่ถูกต้องภายในบล็อก try...catch...finally ซึ่งเป็นเรื่องซ้ำซ้อนและมีโอกาสเกิดข้อผิดพลาดได้ง่าย
  • การผูกติดกัน (Coupling): ตรรกะในการจัดการผลลัพธ์การส่งฟอร์มผูกติดอยู่กับตรรกะการเรนเดอร์ของคอมโพเนนต์อย่างแน่นหนา

ขอแนะนำ useActionState: การเปลี่ยนแปลงกระบวนทัศน์

useActionState คือ hook ของ React ที่ออกแบบมาโดยเฉพาะเพื่อจัดการ state ของการดำเนินการแบบอะซิงโครนัส เช่น การส่งฟอร์ม มันช่วยปรับปรุงกระบวนการทั้งหมดให้ง่ายขึ้นโดยการเชื่อมโยง state เข้ากับผลลัพธ์ของฟังก์ชัน action โดยตรง

รูปแบบการเรียกใช้ (signature) ของมันชัดเจนและรัดกุม:

const [state, formAction] = useActionState(actionFn, initialState);

เรามาดูส่วนประกอบต่างๆ กัน:

  • actionFn(previousState, formData): นี่คือฟังก์ชันอะซิงโครนัสของคุณที่ทำงาน (เช่น การเรียก API) มันจะได้รับ state ก่อนหน้าและข้อมูลฟอร์มเป็นอาร์กิวเมนต์ สิ่งสำคัญคือ สิ่งใดก็ตามที่ฟังก์ชันนี้ ส่งคืน (return) จะกลายเป็น state ใหม่
  • initialState: นี่คือค่าของ state ก่อนที่ action จะถูกดำเนินการเป็นครั้งแรก
  • state: นี่คือ state ปัจจุบัน มันจะเก็บค่า initialState ในตอนเริ่มต้นและจะถูกอัปเดตเป็นค่าที่ส่งคืนจาก actionFn ของคุณหลังจากการดำเนินการแต่ละครั้ง
  • formAction: นี่คือฟังก์ชัน action ของคุณในเวอร์ชันใหม่ที่ถูกครอบไว้ คุณควรส่งฟังก์ชันนี้ไปยัง prop <form> ของอิลิเมนต์ action React จะใช้ฟังก์ชันที่ครอบไว้นี้เพื่อติดตามสถานะ pending ของ action

ตัวอย่างการใช้งานจริง: การ Refactor ด้วย useActionState

ตอนนี้ เรามา refactor ฟอร์มผลิตภัณฑ์ของเราโดยใช้ useActionState กัน การปรับปรุงจะเห็นได้ชัดเจนในทันที

ขั้นแรก เราต้องปรับเปลี่ยนตรรกะของ action ของเรา แทนที่จะโยนข้อผิดพลาด (throw error) action ควรสอนคืนอ็อบเจกต์ state ที่อธิบายผลลัพธ์

ตัวอย่าง: 'วิธีใหม่' ด้วย useActionState

// ฟังก์ชัน action ที่ออกแบบมาเพื่อทำงานกับ useActionState
const addProductAction = async (previousState, formData) => {
const productName = formData.get('productName');
await new Promise(resolve => setTimeout(resolve, 1500)); // จำลองความล่าช้าของเครือข่าย

if (!productName || productName.length < 3) {
return { message: 'Product name must be at least 3 characters long.', success: false };
}

console.log(`Product "${productName}" added.`);
// เมื่อสำเร็จ ให้ส่งคืนข้อความสำเร็จและล้างฟอร์ม
return { message: `Successfully added "${productName}"`, success: true };
};

// คอมโพเนนต์ที่ refactor แล้ว
import { useActionState } from 'react';
// หมายเหตุ: เราจะเพิ่ม useFormStatus ในส่วนถัดไปเพื่อจัดการสถานะ pending

function NewProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);

return (





{!state.success && state.message && (

{state.message}


)}
{state.success && state.message && (

{state.message}


)}

);
}

ดูสิว่ามันสะอาดขึ้นแค่ไหน! เราได้แทนที่ hook useState สามตัวด้วย hook useActionState เพียงตัวเดียว ความรับผิดชอบของคอมโพเนนต์ตอนนี้มีเพียงแค่การเรนเดอร์ UI ตามอ็อบเจกต์ `state` เท่านั้น ตรรกะทางธุรกิจทั้งหมดถูกห่อหุ้มไว้อย่างเรียบร้อยภายในฟังก์ชัน `addProductAction` การอัปเดต state จะเกิดขึ้นโดยอัตโนมัติตามสิ่งที่ action ส่งคืน

แต่เดี๋ยวก่อน แล้วสถานะ pending ล่ะ? เราจะปิดการใช้งานปุ่มในขณะที่ฟอร์มกำลังส่งข้อมูลได้อย่างไร?

การจัดการสถานะ Pending ด้วย useFormStatus

React มี hook ที่มาคู่กันคือ useFormStatus ซึ่งออกแบบมาเพื่อแก้ปัญหานี้โดยเฉพาะ มันให้ข้อมูลสถานะสำหรับการส่งฟอร์มครั้งล่าสุด แต่มีกฎสำคัญคือ: มันจะต้องถูกเรียกจากคอมโพเนนต์ที่ถูกเรนเดอร์อยู่ภายใน <form> ที่คุณต้องการติดตามสถานะ

สิ่งนี้ส่งเสริมการแบ่งแยกหน้าที่ความรับผิดชอบอย่างชัดเจน คุณสร้างคอมโพเนนต์เฉพาะสำหรับองค์ประกอบ UI ที่ต้องรับรู้สถานะการส่งของฟอร์ม เช่น ปุ่ม submit

hook useFormStatus จะส่งคืนอ็อบเจกต์ที่มีคุณสมบัติหลายอย่าง โดยที่สำคัญที่สุดคือ `pending`

const { pending, data, method, action } = useFormStatus();

  • pending: ค่า boolean ที่เป็น `true` หากฟอร์มแม่ (parent form) กำลังส่งข้อมูลอยู่ และเป็น `false` ในกรณีอื่น
  • data: อ็อบเจกต์ `FormData` ที่มีข้อมูลที่กำลังถูกส่ง
  • method: สตริงที่ระบุ HTTP method (`'get'` หรือ `'post'`)
  • action: การอ้างอิงถึงฟังก์ชันที่ส่งไปยัง prop `action` ของฟอร์ม

การสร้างปุ่ม Submit ที่รับรู้สถานะได้

เรามาสร้างคอมโพเนนต์ `SubmitButton` โดยเฉพาะและรวมเข้ากับฟอร์มของเรากัน

ตัวอย่าง: คอมโพเนนต์ SubmitButton

import { useFormStatus } from 'react-dom';
// หมายเหตุ: useFormStatus ถูก import มาจาก 'react-dom' ไม่ใช่ 'react'

function SubmitButton() {
const { pending } = useFormStatus();

return (

);
}

ตอนนี้ เราสามารถอัปเดตคอมโพเนนต์ฟอร์มหลักของเราเพื่อใช้งานมันได้

ตัวอย่าง: ฟอร์มที่สมบูรณ์ด้วย useActionState และ useFormStatus

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';

// ... (ฟังก์ชัน addProductAction ยังคงเหมือนเดิม)

function SubmitButton() { /* ... ตามที่กำหนดไว้ข้างต้น ... */ }

function CompleteProductForm() {
const initialState = { message: null, success: false };
const [state, formAction] = useActionState(addProductAction, initialState);

return (



{/* เราสามารถเพิ่ม key เพื่อรีเซ็ตอินพุตเมื่อสำเร็จ */}


{!state.success && state.message && (

{state.message}


)}
{state.success && state.message && (

{state.message}


)}

);
}

ด้วยโครงสร้างนี้ คอมโพเนนต์ `CompleteProductForm` ไม่จำเป็นต้องรู้อะไรเกี่ยวกับสถานะ pending เลย คอมโพเนนต์ `SubmitButton` นั้นสมบูรณ์ในตัวเอง รูปแบบองค์ประกอบนี้ทรงพลังอย่างเหลือเชื่อสำหรับการสร้าง UI ที่ซับซ้อนและบำรุงรักษาง่าย

พลังของ Progressive Enhancement

หนึ่งในประโยชน์ที่ลึกซึ้งที่สุดของแนวทางใหม่ที่อิงตาม action นี้ โดยเฉพาะอย่างยิ่งเมื่อใช้กับ Server Actions คือ progressive enhancement อัตโนมัติ นี่เป็นแนวคิดที่สำคัญสำหรับการสร้างแอปพลิเคชันสำหรับผู้ชมทั่วโลก ซึ่งสภาพเครือข่ายอาจไม่น่าเชื่อถือและผู้ใช้อาจมีอุปกรณ์รุ่นเก่าหรือปิดใช้งาน JavaScript

นี่คือวิธีการทำงานของมัน:

  1. เมื่อไม่มี JavaScript: หากเบราว์เซอร์ของผู้ใช้ไม่ได้รัน JavaScript ฝั่งไคลเอ็นต์ <form action={...}> จะทำงานเหมือนฟอร์ม HTML มาตรฐาน มันจะทำการร้องขอแบบเต็มหน้า (full-page request) ไปยังเซิร์ฟเวอร์ หากคุณใช้เฟรมเวิร์กอย่าง Next.js, server-side action จะทำงาน และเฟรมเวิร์กจะเรนเดอร์หน้าทั้งหมดใหม่พร้อมกับ state ใหม่ (เช่น แสดงข้อผิดพลาดในการตรวจสอบความถูกต้อง) แอปพลิเคชันจะทำงานได้อย่างสมบูรณ์ เพียงแต่ไม่มีความราบรื่นแบบ SPA
  2. เมื่อมี JavaScript: เมื่อ JavaScript bundle โหลดและ React ทำการ hydrate หน้าเว็บแล้ว `formAction` เดียวกันนั้นจะถูกดำเนินการฝั่งไคลเอ็นต์ แทนที่จะเป็นการรีโหลดทั้งหน้า มันจะทำงานเหมือนกับการร้องขอ fetch ทั่วไป action จะถูกเรียก state จะถูกอัปเดต และเฉพาะส่วนที่จำเป็นของคอมโพเนนต์เท่านั้นที่จะถูกเรนเดอร์ใหม่

ซึ่งหมายความว่าคุณเขียนตรรกะของฟอร์มเพียงครั้งเดียว และมันจะทำงานได้อย่างราบรื่นในทั้งสองสถานการณ์ คุณสร้างแอปพลิเคชันที่ยืดหยุ่นและเข้าถึงได้ง่ายโดยปริยาย ซึ่งเป็นชัยชนะครั้งใหญ่สำหรับประสบการณ์ผู้ใช้ทั่วโลก

รูปแบบการใช้งานขั้นสูงและกรณีศึกษา

1. Server Actions กับ Client Actions

`actionFn` ที่คุณส่งไปยัง useActionState สามารถเป็นฟังก์ชัน async ฝั่งไคลเอ็นต์มาตรฐาน (ดังในตัวอย่างของเรา) หรือ Server Action ก็ได้ Server Action คือฟังก์ชันที่กำหนดบนเซิร์ฟเวอร์ที่สามารถเรียกได้โดยตรงจากคอมโพเน็นต์ฝั่งไคลเอ็นต์ ในเฟรมเวิร์กอย่าง Next.js คุณสามารถกำหนดได้โดยการเพิ่ม directive "use server"; ที่ด้านบนของฟังก์ชัน

  • Client Actions: เหมาะสำหรับการเปลี่ยนแปลงที่ส่งผลต่อ state ฝั่งไคลเอ็นต์เท่านั้น หรือการเรียก API ของบุคคลที่สามโดยตรงจากไคลเอ็นต์
  • Server Actions: เหมาะอย่างยิ่งสำหรับการเปลี่ยนแปลงที่เกี่ยวข้องกับฐานข้อมูลหรือทรัพยากรฝั่งเซิร์ฟเวอร์อื่นๆ มันช่วยลดความซับซ้อนของสถาปัตยกรรมของคุณโดยไม่จำเป็นต้องสร้าง API endpoint ด้วยตนเองสำหรับทุกๆ การเปลี่ยนแปลง

ความสวยงามคือ useActionState ทำงานเหมือนกันทั้งสองแบบ คุณสามารถสลับ client action เป็น server action ได้โดยไม่ต้องเปลี่ยนโค้ดของคอมโพเนนต์

2. การอัปเดตเชิงบวก (Optimistic Updates) ด้วย `useOptimistic`

เพื่อความรู้สึกที่ตอบสนองมากยิ่งขึ้น คุณสามารถรวม useActionState เข้ากับ hook useOptimistic ได้ การอัปเดตเชิงบวกคือการที่คุณอัปเดต UI ทันที โดย *สมมติ* ว่าการดำเนินการแบบอะซิงโครนัสจะสำเร็จ หากล้มเหลว คุณจะย้อน UI กลับไปสู่สถานะก่อนหน้า

ลองนึกภาพแอปโซเชียลมีเดียที่คุณเพิ่มความคิดเห็น ในเชิงบวก คุณจะแสดงความคิดเห็นใหม่ในรายการทันทีในขณะที่คำขอกำลังถูกส่งไปยังเซิร์ฟเวอร์ useOptimistic ถูกออกแบบมาเพื่อทำงานควบคู่กับ actions เพื่อทำให้รูปแบบนี้ง่ายต่อการนำไปใช้

3. การรีเซ็ตฟอร์มเมื่อสำเร็จ

ข้อกำหนดทั่วไปคือการล้างข้อมูลในอินพุตของฟอร์มหลังจากการส่งสำเร็จ มีหลายวิธีที่จะทำได้ด้วย useActionState

  • เทคนิค Key Prop: ดังที่แสดงในตัวอย่าง `CompleteProductForm` ของเรา คุณสามารถกำหนด `key` ที่ไม่ซ้ำกันให้กับอินพุตหรือทั้งฟอร์มได้ เมื่อ key เปลี่ยนแปลง React จะ unmount คอมโพเนนต์เก่าและ mount คอมโพเนนต์ใหม่ ซึ่งเป็นการรีเซ็ต state ของมันอย่างมีประสิทธิภาพ การผูก key เข้ากับแฟล็กความสำเร็จ (`key={state.success ? 'success' : 'initial'}`) เป็นวิธีที่ง่ายและมีประสิทธิภาพ
  • Controlled Components: คุณยังสามารถใช้ controlled components ได้หากจำเป็น โดยการจัดการค่าของอินพุตด้วย useState คุณสามารถเรียกใช้ฟังก์ชัน setter เพื่อล้างค่าภายใน useEffect ที่คอยฟังสถานะความสำเร็จจาก useActionState

ข้อควรระวังและแนวทางปฏิบัติที่ดีที่สุด

  • ตำแหน่งของ useFormStatus: โปรดจำไว้ว่า คอมโพเนนต์ที่เรียก useFormStatus จะต้องถูกเรนเดอร์เป็นลูกของ <form> มันจะไม่ทำงานหากเป็นพี่น้อง (sibling) หรือพ่อแม่ (parent)
  • State ที่ต้องเป็น Serializable: เมื่อใช้ Server Actions อ็อบเจกต์ state ที่ส่งคืนจาก action ของคุณจะต้องเป็น serializable ซึ่งหมายความว่ามันไม่สามารถมีฟังก์ชัน, Symbols, หรือค่าอื่นๆ ที่ไม่สามารถ serialize ได้ ให้ใช้แค่อ็อบเจกต์ธรรมดา, อาร์เรย์, สตริง, ตัวเลข, และบูลีน
  • อย่า Throw ใน Actions: แทนที่จะใช้ `throw new Error()` ฟังก์ชัน action ของคุณควรจัดการกับข้อผิดพลาดอย่างนุ่มนวลและส่งคืนอ็อบเจกต์ state ที่อธิบายข้อผิดพลาด (เช่น `{ success: false, message: 'An error occurred' }`) เพื่อให้แน่ใจว่า state จะถูกอัปเดตอย่างคาดเดาได้เสมอ
  • กำหนดรูปทรงของ State ที่ชัดเจน: สร้างโครงสร้างที่สอดคล้องกันสำหรับอ็อบเจกต์ state ของคุณตั้งแต่เริ่มต้น รูปทรงเช่น `{ data: T | null, message: string | null, success: boolean, errors: Record | null }` สามารถครอบคลุมกรณีการใช้งานได้หลากหลาย

useActionState เทียบกับ useReducer: การเปรียบเทียบอย่างรวดเร็ว

ในแวบแรก useActionState อาจดูคล้ายกับ useReducer เนื่องจากทั้งสองเกี่ยวข้องกับการอัปเดต state โดยอิงจาก state ก่อนหน้า อย่างไรก็ตาม พวกมันมีวัตถุประสงค์ที่แตกต่างกัน

  • useReducer เป็น hook อเนกประสงค์สำหรับการจัดการการเปลี่ยนแปลง state ที่ซับซ้อนฝั่ง client-side มันถูกกระตุ้นโดยการ dispatch actions และเหมาะสำหรับตรรกะ state ที่มีการเปลี่ยนแปลง state แบบ synchronous ที่เป็นไปได้หลายอย่าง (เช่น wizard หลายขั้นตอนที่ซับซ้อน)
  • useActionState เป็น hook เฉพาะทางที่ออกแบบมาสำหรับ state ที่เปลี่ยนแปลงเพื่อตอบสนองต่อ action แบบอะซิงโครนัสเพียงครั้งเดียว บทบาทหลักของมันคือการรวมเข้ากับฟอร์ม HTML, Server Actions, และคุณสมบัติการเรนเดอร์พร้อมกันของ React เช่น การเปลี่ยนสถานะ pending

ข้อสรุป: สำหรับการส่งฟอร์มและการดำเนินการแบบอะซิงโครนัสที่ผูกกับฟอร์ม useActionState คือเครื่องมือที่ทันสมัยและสร้างขึ้นเพื่อวัตถุประสงค์นี้โดยเฉพาะ สำหรับ state machine ที่ซับซ้อนอื่นๆ ฝั่งไคลเอ็นต์ useReducer ยังคงเป็นตัวเลือกที่ยอดเยี่ยม

บทสรุป: ก้าวสู่อนาคตของฟอร์มใน React

hook useActionState เป็นมากกว่าแค่ API ใหม่ มันแสดงถึงการเปลี่ยนแปลงพื้นฐานไปสู่วิธีการจัดการฟอร์มและการเปลี่ยนแปลงข้อมูลใน React ที่แข็งแกร่งขึ้น เป็นแบบ declarative และเน้นผู้ใช้เป็นศูนย์กลางมากขึ้น โดยการนำไปใช้ คุณจะได้รับ:

  • ลด Boilerplate: hook เดียวแทนที่การเรียก useState หลายครั้งและการจัดการ state ด้วยตนเอง
  • สถานะ Pending ในตัว: จัดการ UI การโหลดได้อย่างราบรื่นด้วย hook คู่หู useFormStatus
  • Progressive Enhancement ในตัว: เขียนโค้ดที่ทำงานได้ทั้งแบบมีหรือไม่มี JavaScript ทำให้มั่นใจได้ถึงการเข้าถึงและความยืดหยุ่นสำหรับผู้ใช้ทุกคน
  • การสื่อสารกับเซิร์ฟเวอร์ที่ง่ายขึ้น: เหมาะอย่างยิ่งสำหรับ Server Actions ทำให้ประสบการณ์การพัฒนา full-stack ง่ายขึ้น

เมื่อคุณเริ่มโปรเจกต์ใหม่หรือ refactor โปรเจกต์ที่มีอยู่ ลองพิจารณาใช้ useActionState มันไม่เพียงแต่จะปรับปรุงประสบการณ์ของนักพัฒนาโดยทำให้โค้ดของคุณสะอาดขึ้นและคาดเดาได้ง่ายขึ้น แต่ยังช่วยให้คุณสร้างแอปพลิเคชันคุณภาพสูงที่เร็วขึ้น ยืดหยุ่นมากขึ้น และเข้าถึงได้โดยผู้ชมทั่วโลกที่หลากหลาย