สำรวจการใช้ useActionState ของ React ร่วมกับ state machine เพื่อสร้าง UI ที่แข็งแกร่งและคาดเดาได้ เรียนรู้ตรรกะการเปลี่ยนสถานะของ action สำหรับแอปพลิเคชันที่ซับซ้อน
React useActionState State Machine: การเชี่ยวชาญตรรกะการเปลี่ยนสถานะของ Action
useActionState
ของ React เป็น Hook ที่ทรงพลังซึ่งเปิดตัวใน React 19 (ปัจจุบันอยู่ในเวอร์ชัน canary) ออกแบบมาเพื่อลดความซับซ้อนของการอัปเดต state แบบ asynchronous โดยเฉพาะเมื่อต้องจัดการกับ server actions เมื่อใช้ร่วมกับ state machine มันมอบวิธีการที่สวยงามและแข็งแกร่งในการจัดการปฏิสัมพันธ์ UI ที่ซับซ้อนและการเปลี่ยนสถานะ (state transitions) บล็อกโพสต์นี้จะเจาะลึกถึงวิธีการใช้ useActionState
ร่วมกับ state machine อย่างมีประสิทธิภาพเพื่อสร้างแอปพลิเคชัน React ที่คาดเดาได้และบำรุงรักษาง่าย
State Machine คืออะไร?
State machine คือแบบจำลองทางคณิตศาสตร์ของการคำนวณที่อธิบายพฤติกรรมของระบบว่ามีสถานะ (state) และการเปลี่ยนผ่าน (transition) ระหว่างสถานะเหล่านั้นในจำนวนจำกัด แต่ละสถานะแสดงถึงเงื่อนไขที่แตกต่างกันของระบบ และการเปลี่ยนผ่านแสดงถึงเหตุการณ์ที่ทำให้ระบบย้ายจากสถานะหนึ่งไปยังอีกสถานะหนึ่ง ลองนึกภาพว่ามันเหมือนกับ flowchart แต่มีกฎที่เข้มงวดกว่าเกี่ยวกับวิธีการที่คุณสามารถย้ายไปมาระหว่างขั้นตอนต่างๆ ได้
การใช้ state machine ในแอปพลิเคชัน React ของคุณมีประโยชน์หลายประการ:
- ความสามารถในการคาดเดา (Predictability): State machine บังคับให้มีการควบคุมการทำงานที่ชัดเจนและคาดเดาได้ ทำให้ง่ายต่อการทำความเข้าใจพฤติกรรมของแอปพลิเคชันของคุณ
- การบำรุงรักษา (Maintainability): ด้วยการแยกตรรกะของ state ออกจากการเรนเดอร์ UI ทำให้ state machine ช่วยปรับปรุงการจัดระเบียบโค้ดและทำให้ง่ายต่อการบำรุงรักษาและอัปเดตแอปพลิเคชันของคุณ
- ความสามารถในการทดสอบ (Testability): State machine สามารถทดสอบได้โดยเนื้อแท้ เพราะคุณสามารถกำหนดพฤติกรรมที่คาดหวังสำหรับแต่ละสถานะและการเปลี่ยนผ่านได้อย่างง่ายดาย
- การแสดงผลเป็นภาพ (Visual Representation): State machine สามารถแสดงผลเป็นภาพได้ ซึ่งช่วยในการสื่อสารพฤติกรรมของแอปพลิเคชันกับนักพัฒนาคนอื่น ๆ หรือผู้มีส่วนได้ส่วนเสีย
แนะนำ useActionState
Hook useActionState
ช่วยให้คุณสามารถจัดการผลลัพธ์ของ action ที่อาจเปลี่ยนแปลงสถานะของแอปพลิเคชันได้ มันถูกออกแบบมาเพื่อทำงานร่วมกับ server actions ได้อย่างราบรื่น แต่ก็สามารถปรับใช้กับ client-side actions ได้เช่นกัน มันมอบวิธีที่สะอาดในการจัดการสถานะการโหลด (loading states) ข้อผิดพลาด (errors) และผลลัพธ์สุดท้ายของ action ทำให้ง่ายต่อการสร้าง UI ที่ตอบสนองและเป็นมิตรต่อผู้ใช้
นี่คือตัวอย่างพื้นฐานของวิธีการใช้ useActionState
:
const [state, dispatch] = useActionState(async (prevState, formData) => {
// ตรรกะของ action ของคุณที่นี่
try {
const result = await someAsyncFunction(formData);
return { ...prevState, data: result };
} catch (error) {
return { ...prevState, error: error.message };
}
}, { data: null, error: null });
ในตัวอย่างนี้:
- อาร์กิวเมนต์แรกคือฟังก์ชัน asynchronous ที่ดำเนินการ action มันจะได้รับ state ก่อนหน้าและข้อมูลฟอร์ม (ถ้ามี)
- อาร์กิวเมนต์ที่สองคือ state เริ่มต้น
- Hook จะคืนค่าเป็นอาร์เรย์ที่ประกอบด้วย state ปัจจุบันและฟังก์ชัน dispatch
การรวม useActionState
และ State Machines เข้าด้วยกัน
พลังที่แท้จริงมาจากการรวม useActionState
เข้ากับ state machine ซึ่งช่วยให้คุณสามารถกำหนดการเปลี่ยนสถานะที่ซับซ้อนซึ่งถูกกระตุ้นโดย asynchronous actions ลองพิจารณาสถานการณ์: คอมโพเนนต์ e-commerce ง่ายๆ ที่ดึงข้อมูลรายละเอียดสินค้า
ตัวอย่าง: การดึงข้อมูลรายละเอียดสินค้า
เราจะกำหนดสถานะต่อไปนี้สำหรับคอมโพเนนต์รายละเอียดสินค้าของเรา:
- Idle: สถานะเริ่มต้น ยังไม่มีการดึงข้อมูลรายละเอียดสินค้า
- Loading: สถานะระหว่างที่กำลังดึงข้อมูลรายละเอียดสินค้า
- Success: สถานะหลังจากดึงข้อมูลรายละเอียดสินค้าสำเร็จแล้ว
- Error: สถานะหากเกิดข้อผิดพลาดระหว่างการดึงข้อมูลรายละเอียดสินค้า
เราสามารถแสดง state machine นี้โดยใช้อ็อบเจ็กต์:
const productDetailsMachine = {
initial: 'idle',
states: {
idle: {
on: {
FETCH: 'loading',
},
},
loading: {
on: {
SUCCESS: 'success',
ERROR: 'error',
},
},
success: {
type: 'final',
},
error: {
on: {
FETCH: 'loading',
},
},
},
};
นี่เป็นการแสดงผลแบบง่าย ไลบรารีอย่าง XState มีการใช้งาน state machine ที่ซับซ้อนกว่าพร้อมคุณสมบัติต่างๆ เช่น สถานะแบบลำดับชั้น (hierarchical states), สถานะแบบขนาน (parallel states) และ guards
การนำไปใช้ใน React
ตอนนี้ เราจะนำ state machine นี้ไปรวมกับ useActionState
ในคอมโพเนนต์ React
import React from 'react';
// ติดตั้ง XState หากคุณต้องการประสบการณ์ state machine เต็มรูปแบบ สำหรับตัวอย่างพื้นฐานนี้ เราจะใช้อ็อบเจ็กต์ธรรมดา
// import { createMachine, useMachine } from 'xstate';
const productDetailsMachine = {
initial: 'idle',
states: {
idle: {
on: {
FETCH: 'loading',
},
},
loading: {
on: {
SUCCESS: 'success',
ERROR: 'error',
},
},
success: {
type: 'final',
},
error: {
on: {
FETCH: 'loading',
},
},
},
};
function ProductDetails({ productId }) {
const [state, dispatch] = React.useReducer(
(state, event) => {
const nextState = productDetailsMachine.states[state].on[event];
return nextState || state; // คืนค่าสถานะถัดไป หรือสถานะปัจจุบันหากไม่มีการเปลี่ยนผ่านที่กำหนดไว้
},
productDetailsMachine.initial
);
const [productData, setProductData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
if (state === 'loading') {
const fetchData = async () => {
try {
const response = await fetch(`https://api.example.com/products/${productId}`); // แทนที่ด้วย API endpoint ของคุณ
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setProductData(data);
setError(null);
dispatch('SUCCESS');
} catch (e) {
setError(e.message);
setProductData(null);
dispatch('ERROR');
}
};
fetchData();
}
}, [state, productId, dispatch]);
const handleFetch = () => {
dispatch('FETCH');
};
return (
รายละเอียดสินค้า
{state === 'idle' && }
{state === 'loading' && กำลังโหลด...
}
{state === 'success' && (
{productData.name}
{productData.description}
ราคา: ${productData.price}
)}
{state === 'error' && ข้อผิดพลาด: {error}
}
);
}
export default ProductDetails;
คำอธิบาย:
- เรากำหนด
productDetailsMachine
เป็นอ็อบเจ็กต์ JavaScript ง่ายๆ เพื่อแทน state machine ของเรา - เราใช้
React.useReducer
เพื่อจัดการการเปลี่ยนสถานะตาม machine ของเรา - เราใช้ Hook
useEffect
ของ React เพื่อกระตุ้นการดึงข้อมูลเมื่อสถานะเป็น 'loading' - ฟังก์ชัน
handleFetch
จะ dispatch เหตุการณ์ 'FETCH' เพื่อเริ่มต้นสถานะการโหลด - คอมโพเนนต์จะเรนเดอร์เนื้อหาที่แตกต่างกันไปตามสถานะปัจจุบัน
การใช้ useActionState
(สมมติ - ฟีเจอร์ของ React 19)
ในขณะที่ useActionState
ยังไม่เปิดให้ใช้งานอย่างเต็มที่ นี่คือลักษณะการใช้งานเมื่อพร้อมใช้งาน ซึ่งเป็นแนวทางที่สะอาดกว่า:
import React from 'react';
//import { useActionState } from 'react'; // ยกเลิกคอมเมนต์เมื่อพร้อมใช้งาน
const productDetailsMachine = {
initial: 'idle',
states: {
idle: {
on: {
FETCH: 'loading',
},
},
loading: {
on: {
SUCCESS: 'success',
ERROR: 'error',
},
},
success: {
type: 'final',
},
error: {
on: {
FETCH: 'loading',
},
},
},
};
function ProductDetails({ productId }) {
const initialState = { state: productDetailsMachine.initial, data: null, error: null };
// การใช้งาน useActionState แบบสมมติ
const [newState, dispatch] = React.useReducer(
(state, event) => {
const nextState = productDetailsMachine.states[state.state].on[event];
return nextState ? { ...state, state: nextState } : state; // คืนค่าสถานะถัดไป หรือสถานะปัจจุบันหากไม่มีการเปลี่ยนผ่านที่กำหนดไว้
},
initialState
);
const handleFetchProduct = async () => {
dispatch('FETCH');
try {
const response = await fetch(`https://api.example.com/products/${productId}`); // แทนที่ด้วย API endpoint ของคุณ
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// ดึงข้อมูลสำเร็จ - dispatch SUCCESS พร้อมข้อมูล!
dispatch('SUCCESS');
// บันทึกข้อมูลที่ดึงมาไปยัง local state ไม่สามารถใช้ dispatch ภายใน reducer ได้
newState.data = data; // อัปเดตนอก dispatcher
} catch (error) {
// เกิดข้อผิดพลาด - dispatch ERROR พร้อมข้อความแสดงข้อผิดพลาด!
dispatch('ERROR');
// จัดเก็บข้อผิดพลาดในตัวแปรใหม่เพื่อแสดงผลใน render()
newState.error = error.message;
}
//}, initialState);
};
return (
รายละเอียดสินค้า
{newState.state === 'idle' && }
{newState.state === 'loading' && กำลังโหลด...
}
{newState.state === 'success' && newState.data && (
{newState.data.name}
{newState.data.description}
ราคา: ${newState.data.price}
)}
{newState.state === 'error' && newState.error && ข้อผิดพลาด: {newState.error}
}
);
}
export default ProductDetails;
หมายเหตุสำคัญ: ตัวอย่างนี้เป็นเพียงสมมติฐานเนื่องจาก useActionState
ยังไม่พร้อมใช้งานอย่างเต็มที่และ API ที่แน่นอนอาจมีการเปลี่ยนแปลง ผมได้แทนที่ด้วย useReducer มาตรฐานเพื่อให้ตรรกะหลักทำงานได้ อย่างไรก็ตาม ความตั้งใจคือเพื่อแสดงให้เห็นว่าคุณ *จะ* ใช้งานมันอย่างไร หากมันพร้อมใช้งานและคุณต้องแทนที่ useReducer ด้วย useActionState ในอนาคตเมื่อมี useActionState
โค้ดนี้ควรจะทำงานได้ตามที่อธิบายโดยมีการเปลี่ยนแปลงเพียงเล็กน้อย ซึ่งจะช่วยลดความซับซ้อนในการจัดการข้อมูลแบบ asynchronous ได้อย่างมาก
ประโยชน์ของการใช้ useActionState
ร่วมกับ State Machines
- การแยกส่วนความรับผิดชอบที่ชัดเจน (Clear Separation of Concerns): ตรรกะของ state ถูกห่อหุ้มอยู่ภายใน state machine ในขณะที่การเรนเดอร์ UI ถูกจัดการโดยคอมโพเนนต์ React
- ปรับปรุงความสามารถในการอ่านโค้ด (Improved Code Readability): State machine ให้การแสดงผลพฤติกรรมของแอปพลิเคชันเป็นภาพ ทำให้เข้าใจและบำรุงรักษาได้ง่ายขึ้น
- การจัดการ Asynchronous ที่ง่ายขึ้น (Simplified Asynchronous Handling):
useActionState
ช่วยให้การจัดการ asynchronous actions มีความคล่องตัว ลดโค้ด boilerplate ลง - เพิ่มความสามารถในการทดสอบ (Enhanced Testability): State machine สามารถทดสอบได้โดยเนื้อแท้ ช่วยให้คุณสามารถตรวจสอบความถูกต้องของพฤติกรรมของแอปพลิเคชันได้อย่างง่ายดาย
แนวคิดขั้นสูงและข้อควรพิจารณา
การผนวกรวมกับ XState
สำหรับความต้องการจัดการ state ที่ซับซ้อนมากขึ้น ลองพิจารณาใช้ไลบรารี state machine โดยเฉพาะอย่าง XState XState มีเฟรมเวิร์กที่ทรงพลังและยืดหยุ่นสำหรับการกำหนดและจัดการ state machine พร้อมด้วยคุณสมบัติต่างๆ เช่น สถานะแบบลำดับชั้น, สถานะแบบขนาน, guards และ actions
// ตัวอย่างการใช้ XState
import { createMachine, useMachine } from 'xstate';
const productDetailsMachine = createMachine({
id: 'productDetails',
initial: 'idle',
states: {
idle: {
on: {
FETCH: 'loading',
},
},
loading: {
invoke: {
id: 'fetchProduct',
src: (context, event) => fetch(`https://api.example.com/products/${context.productId}`).then(res => res.json()),
onDone: {
target: 'success',
actions: assign({ product: (context, event) => event.data })
},
onError: {
target: 'error',
actions: assign({ error: (context, event) => event.data })
}
}
},
success: {
type: 'final',
},
error: {
on: {
FETCH: 'loading',
},
},
},
}, {
services: {
fetchProduct: (context, event) => fetch(`https://api.example.com/products/${context.productId}`).then(res => res.json())
}
});
สิ่งนี้มอบวิธีการจัดการ state ที่เป็นแบบ declarative และแข็งแกร่งมากขึ้น อย่าลืมติดตั้งโดยใช้: npm install xstate
การจัดการ Global State
สำหรับแอปพลิเคชันที่มีข้อกำหนดในการจัดการ state ที่ซับซ้อนในหลายคอมโพเนนต์ ให้พิจารณาใช้โซลูชันการจัดการ state ส่วนกลาง เช่น Redux หรือ Zustand ร่วมกับ state machine สิ่งนี้ช่วยให้คุณสามารถรวมศูนย์ state ของแอปพลิเคชันและแชร์ระหว่างคอมโพเนนต์ต่างๆ ได้อย่างง่ายดาย
การทดสอบ State Machines
การทดสอบ state machine เป็นสิ่งสำคัญเพื่อให้แน่ใจว่าแอปพลิเคชันของคุณทำงานถูกต้องและเชื่อถือได้ คุณสามารถใช้เฟรมเวิร์กการทดสอบ เช่น Jest หรือ Mocha เพื่อเขียน unit test สำหรับ state machine ของคุณ เพื่อตรวจสอบว่ามันเปลี่ยนผ่านระหว่างสถานะตามที่คาดไว้และจัดการกับเหตุการณ์ต่างๆ ได้อย่างถูกต้อง
นี่คือตัวอย่างง่ายๆ:
// ตัวอย่างการทดสอบด้วย Jest
import { interpret } from 'xstate';
import { productDetailsMachine } from './productDetailsMachine';
describe('productDetailsMachine', () => {
it('should transition from idle to loading on FETCH event', (done) => {
const service = interpret(productDetailsMachine).onTransition((state) => {
if (state.value === 'loading') {
expect(state.value).toBe('loading');
done();
}
});
service.start();
service.send('FETCH');
});
});
การทำให้เป็นสากล (Internationalization - i18n)
เมื่อสร้างแอปพลิเคชันสำหรับผู้ชมทั่วโลก การทำให้เป็นสากล (i18n) เป็นสิ่งจำเป็น ตรวจสอบให้แน่ใจว่าตรรกะของ state machine และการเรนเดอร์ UI ของคุณได้รับการทำให้เป็นสากลอย่างเหมาะสมเพื่อรองรับหลายภาษาและบริบททางวัฒนธรรม พิจารณาสิ่งต่อไปนี้:
- เนื้อหาข้อความ: ใช้ไลบรารี i18n เพื่อแปลเนื้อหาข้อความตาม locale ของผู้ใช้
- รูปแบบวันที่และเวลา: ใช้ไลบรารีการจัดรูปแบบวันที่และเวลาที่รองรับ locale เพื่อแสดงวันที่และเวลาในรูปแบบที่ถูกต้องสำหรับภูมิภาคของผู้ใช้
- รูปแบบสกุลเงิน: ใช้ไลบรารีการจัดรูปแบบสกุลเงินที่รองรับ locale เพื่อแสดงค่าสกุลเงินในรูปแบบที่ถูกต้องสำหรับภูมิภาคของผู้ใช้
- รูปแบบตัวเลข: ใช้ไลบรารีการจัดรูปแบบตัวเลขที่รองรับ locale เพื่อแสดงตัวเลขในรูปแบบที่ถูกต้องสำหรับภูมิภาคของผู้ใช้ (เช่น ตัวคั่นทศนิยม, ตัวคั่นหลักพัน)
- เลย์เอาต์จากขวาไปซ้าย (RTL): รองรับเลย์เอาต์ RTL สำหรับภาษาต่างๆ เช่น อารบิกและฮีบรู
โดยการพิจารณาด้าน i18n เหล่านี้ คุณสามารถมั่นใจได้ว่าแอปพลิเคชันของคุณสามารถเข้าถึงได้และเป็นมิตรต่อผู้ใช้สำหรับผู้ชมทั่วโลก
สรุป
การรวม useActionState
ของ React เข้ากับ state machine นำเสนอแนวทางที่ทรงพลังในการสร้าง UI ที่แข็งแกร่งและคาดเดาได้ ด้วยการแยกตรรกะของ state ออกจากการเรนเดอร์ UI และบังคับให้มีการไหลของการควบคุมที่ชัดเจน state machine ช่วยปรับปรุงการจัดระเบียบโค้ด การบำรุงรักษา และความสามารถในการทดสอบ แม้ว่า useActionState
จะยังเป็นฟีเจอร์ที่กำลังจะมาถึง แต่การทำความเข้าใจวิธีรวม state machine ในตอนนี้จะเตรียมคุณให้พร้อมใช้ประโยชน์จากมันเมื่อพร้อมใช้งาน ไลบรารีอย่าง XState ยังมอบความสามารถในการจัดการ state ที่ล้ำหน้ายิ่งขึ้น ทำให้การจัดการตรรกะของแอปพลิเคชันที่ซับซ้อนง่ายขึ้น
ด้วยการนำ state machine และ useActionState
มาใช้ คุณสามารถยกระดับทักษะการพัฒนา React ของคุณและสร้างแอปพลิเคชันที่เชื่อถือได้ บำรุงรักษาง่าย และเป็นมิตรต่อผู้ใช้ทั่วโลกได้มากขึ้น