ทำไมต้องใช้ useReducer กับ TypeScript ในโปรเจกต์ที่ซับซ้อน
เมื่อเราเริ่มทำแอปที่มีสถานะ (ค่าที่เก็บในโปรแกรม) เยอะขึ้น การใช้ useState แบบเดิมอาจจะจัดการได้ยากขึ้นเรื่อย ๆ useReducer (เครื่องมือจัดการสถานะแบบเป็นลำดับขั้นตอน) จึงเข้ามาช่วยให้เราคุมการเปลี่ยนข้อมูลได้เป็นระเบียบกว่าเดิม
ลองนึกภาพการสั่งอาหารในร้านอาหาร ถ้าเราสั่งทีละอย่างพนักงานอาจจะสับสน แต่ถ้าเรามี ใบสั่งอาหาร (รายการคำสั่งที่ระบุชัดเจน) พนักงานก็จะทำงานได้แม่นยำขึ้น useReducer ก็คือระบบที่เปลี่ยนข้อมูลตาม "ใบสั่ง" นี้เอง
การใช้คู่กับ TypeScript (ภาษาที่ช่วยตรวจสอบความผิดพลาดของโค้ด) จะทำให้เรามั่นใจว่าใบสั่งอาหารของเราไม่มีทางเขียนผิด เช่น ไม่เผลอใส่ข้อมูลที่ไม่จำเป็นเข้าไป ทำให้โค้ดของเราพังยากขึ้นมากตอนที่โปรเจกต์ใหญ่ขึ้น
รู้จักกับ Discriminated Unions เพื่อความปลอดภัยของข้อมูล
Discriminated Unions (การรวมประเภทข้อมูลโดยมีจุดสังเกต) คือเทคนิคการสร้างกฎว่า "ถ้าเลือกคำสั่งนี้ ต้องมีข้อมูลชุดนี้ตามมา" มันช่วยให้เราไม่เผลอใส่ข้อมูลมั่วซั่วลงไปในโปรแกรม
ลองจินตนาการถึงตู้จดหมาย ถ้าเราติดป้ายหน้าตู้ว่า "จดหมายด่วน" ต้องมี "ที่อยู่ผู้ส่ง" แต่ถ้าเป็น "โฆษณา" ไม่ต้องมีที่อยู่ การใช้ type เป็นตัวแยกประเภทข้อมูลแบบนี้ จะช่วยให้โปรแกรมรู้ทันทีว่าต้องเช็กข้อมูลส่วนไหนบ้าง
หากเราไม่ใช้เทคนิคนี้ เวลาเขียนโค้ดเราอาจจะลืมใส่ข้อมูลสำคัญไป ทำให้ตอนรันโปรแกรมแล้วเกิดบั๊ก (ข้อผิดพลาดของโค้ด) ได้ง่าย การกำหนดกฎแบบนี้จึงเป็นเหมือน เกราะป้องกัน ให้เราเขียนโค้ดได้มั่นใจขึ้น
// กำหนดรูปแบบของ Action (คำสั่งที่ส่งไปเปลี่ยนสถานะ)
type TodoAction =
| { type: 'ADD'; payload: { id: number; text: string } }
| { type: 'DELETE'; id: number };
// ตัวอย่างนี้คือการบอกว่า
// ถ้า type เป็น 'ADD' ต้องมี payload
// แต่ถ้าเป็น 'DELETE' ต้องมีแค่ id เท่านั้น
ในโค้ดด้านบน เราได้กำหนดกฎเหล็กไว้แล้วว่าคำสั่ง ADD ต้องมาคู่กับข้อมูล payload (ข้อมูลที่ต้องการเพิ่ม) เสมอ ส่วนคำสั่ง DELETE จะสนใจแค่ id (เลขระบุตัวตนของข้อมูล) เท่านั้น
วิธีเขียน useReducer ให้ทำงานกับกฎของเรา
เมื่อเรามีกฎแล้ว ขั้นตอนถัดไปคือการสร้างฟังก์ชันที่จะคอยรับคำสั่งไปทำงานจริง เราต้องทำให้ฟังก์ชันนี้ฉลาดพอที่จะรู้ว่า "ถ้าได้รับคำสั่งแบบนี้ ต้องทำอย่างไร" โดยใช้ switch (คำสั่งเลือกเงื่อนไข) เป็นตัวตรวจสอบ
- สร้างฟังก์ชัน
reducerที่รับสถานะปัจจุบันและคำสั่งเข้ามา - ใช้
switchเช็กค่าaction.typeเพื่อตัดสินใจว่าจะทำอะไร - ส่งค่าสถานะใหม่กลับออกไปเสมอ เพื่อให้หน้าจอแสดงผลได้ถูกต้อง
function todoReducer(state: Todo[], action: TodoAction) {
switch (action.type) {
case 'ADD':
return [...state, action.payload];
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
return state; // คืนค่าเดิมถ้าไม่เจอคำสั่งที่ตรงกัน
}
}
สังเกตว่า TypeScript จะช่วยเราได้มาก หากเราพยายามส่งข้อมูลผิดกฎใน action ระบบจะแจ้งเตือนทันทีตั้งแต่วันที่เรากำลังเขียนโค้ด ไม่ต้องรอให้ไปพังตอนรันจริง
จุดที่มือใหม่มักพลาดและวิธีแก้ไข
สิ่งที่มือใหม่มักพลาดคือการลืมใส่ default (กรณีที่ไม่ตรงกับเงื่อนไขใดเลย) ใน switch หรือลืมระบุประเภทข้อมูลของ state ให้ชัดเจนตั้งแต่ต้น ทำให้โปรแกรมทำงานไม่สมบูรณ์
อีกเรื่องคือการพยายามเปลี่ยนค่าสถานะเดิมโดยตรง ซึ่งถือเป็นเรื่องห้ามทำใน React (เครื่องมือสร้างหน้าเว็บ) เราต้องสร้างข้อมูลชุดใหม่ขึ้นมาเสมอ เช่น การใช้ ...state (การก๊อปปี้ข้อมูลเดิม) เพื่อสร้างรายการใหม่
ถ้าคุณกำลังฝึกเขียน ให้ลองเริ่มจากโปรเจกต์เล็ก ๆ อย่าง To-Do List (รายการสิ่งที่ต้องทำ) ก่อน อย่าเพิ่งรีบข้ามไปทำระบบที่ซับซ้อนเกินไป การฝึกให้ชินกับกฎของ useReducer จะทำให้คุณทำงานกับทีมได้ง่ายขึ้นในอนาคต
สรุป: เอาไปใช้จริงในงานพัฒนา
การใช้ Discriminated Unions ร่วมกับ useReducer เป็นทักษะสำคัญที่ทำให้โค้ดของคุณดูเป็นมืออาชีพ มันช่วยลดบั๊กที่เกิดจากการลืมใส่ข้อมูล และทำให้คนอื่นในทีมอ่านโค้ดของคุณได้ง่ายขึ้นมาก
ลองนำไปประยุกต์ใช้กับโปรเจกต์ถัดไปของคุณ เช่น ระบบตะกร้าสินค้าที่มีคำสั่ง ADD_ITEM หรือ REMOVE_ITEM คุณจะพบว่าการจัดการข้อมูลที่ซับซ้อนกลายเป็นเรื่องที่ควบคุมได้ง่ายกว่าเดิมเยอะ
การเป็นโปรแกรมเมอร์ที่ดีไม่ได้วัดกันที่เขียนโค้ดเร็วแค่ไหน แต่อยู่ที่การเขียนโค้ดให้ อ่านง่ายและแก้ไขได้ปลอดภัย ในระยะยาวครับ ขอให้สนุกกับการฝึกฝนและทำความเข้าใจเครื่องมือเหล่านี้ให้แม่นยำนะครับ