วิธีออกแบบ State structure ที่ดีใน React
State structure ของ React ถูกออกแบบให้มีความยืดหยุ่นสูงสามารถทำได้หลายวิธีมาก เพื่อให้เหมาะสมกับระบบของเรา ซึ่งเป็นเรื่องที่สำคัญมากเรื่องนึงที่จะทำให้ระบบของเรานั้นซับซ้อนเกินความจำเป็นหรือไม่ ถ้าเราออกแบบอย่างไม่ถูกวิธี
ช่วงนี้กระแส alternative frontend framework กำลังมาแรง ผมก็อยากสวนกระแส ด้วยการลอง Relearn React ใหม่อีกครั้งเพื่อทบทวนสิ่งที่เคยเรียนรู้ทั้งหมด จึงได้มาเจอกับ document ของ React version Beta ที่กำลังพัฒนาอยู่ ซึ่ง Choosing the State Structure เป็นหนึ่งในหัวข้อนั้น หลังจากอ่านจบแล้วเลยอยากลองสรุปกับตัวเองอีกครั้งดู
ซึ่งหลักการสำคัญในการออกแบบ State Structure ที่ดีนั้น คือการออกแบบที่ทำให้ง่ายต่อการแก้ไข และ ลดโอกาสผิดพลาด และความซ้ำซ้อนให้ได้มากที่สุด ซึ่งมีหลักการคล้ายกับการ normalized Database Strcture ของ Database Engineer เลย โดยสามารถสรุปออกมาเป็น 5 ข้อด้วยกัน คือ
- Group related state. ถ้าเรามี state ที่จะมีการเปลี่ยนแปลงค่าพร้อมกันเสมอก็
ควรจะรวมกันเป็น object เดียวกัน
เช่น ถ้าเราสร้าง state x, y ที่จะเป็นค่าตำแหน่งของ element เอาไว้
const [x, setX] = useState(0); ❌
const [y, setY] = useState(0); ❌
ซึ่งค่า x และ y ควรที่จะต้องแก้ไขด้วยกันเสมอ เราสามารถรวมกันเป็น object เดียวกันได้เพื่อให้ง่ายต่อการแก้ไข
const [position, setPosition] = useState({ x: 0, y: 0 }); ✅ // group related state
2. Avoid contradictions in state. ถ้าเราสร้าง state ใช้ร่วมกัน แต่แยกกันมากเกินไปอาจจะทำให้เกิดความสับสนใจการใช้งานได้
ถ้าเราสร้าง state isSending
และกับ isSent
ในการ handle การส่งข้อมูล จะทำให้สับสนได้ว่า ถ้า isSending=true
และ isSent=true
หมายความว่าอย่างไร และเกิดการเข้าใจผิดได้
const [isSending, setIsSending] = useState(false); ❌
const [isSent, setIsSent] = useState(false); ❌
เราสามารถสร้าง state status
เพื่อ handle การส่งข้อมูลทั้งหมดได้ และให้ isSending
และ isSent
คำนวณจาก state status
อีกที
const [status, setStatus] = useState('idle'); ✅
const isSending = status === 'sending'; // calculate from status state
const isSent = status === 'sent';// calculate from status state
3. Avoid redundant state. ถ้ามี state ที่ต้องคำนวณจาก props หรือ state อื่นเสมอ
เช่น state fullName
จะเกิดจากการรวมกันของ firstName
กับ lastName
เสมอ
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); ❌ // redundant state
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName); ❌ // redundant state
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value); ❌ // redundant state
}
เราสามารถย้ายไปคำนวณช่วง rendering ได้โดยที่ไม่จำเป็นต้องใช้ state เลย สามารถลด state ที่ไม่จำเป็นไปได้
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName; ✅ // calculate during rendering
4. Avoid duplication in state. เมื่อมีข้อมูลที่ซ้ำกันอยู่คนละ state จะทำให้การ sync ข้อมูลเพื่ออัพเดตข้อมูลสามารถทำได้ยาก เราจึงไม่ควรที่จะเก็บข้อมูลที่ซ้ำกันคนละ state
เช่น ถ้าเราเก็บ selectedItem
เป็น object structure เดียวกับ items
เลยถ้าเรามี feature ต้องสามารถแก้ไขข้อมูลใน selectedItem
ได้ เราจะต้องกลับไป sync ที่ state ของ items
ด้วยเพื่อให้ข้อมูลตรงกัน ซึ่งทำได้ยาก
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]); ❌ // duplicate
เราสามารถเก็บ selectedId
แทน object ทั้งหมดได้ และให้ selectedItem
คำนวณตอน rendering แทน ทำให้สามารถลดความซ้ำซ้อนของข้อมูลได้
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0); // store only id
const selectedItem = items.find(item => item.id === selectedId); ✅
5. Avoid deeply nested state. การเก็บข้อมูลเป็น object หลายๆชั้นทำให้ยากต่อการอัพเดตข้อมูล เราไม่ควรที่จะเก็บข้อมูลหลายๆชั้นเกินไป
เช่น การเก็บข้อมูล initialTravelPlan
เป็น object ขนาดใหญ่ที่รวมข้อมูลของ childPlaces
ไว้ด้วย จะทำให้การแก้ไข หรือ ลบข้อมูล ทำได้ยากและซับซ้อน
const initialTravelPlan = [{
id: 0,
title: '(Root)',
childPlaces: [{ ❌ // nested state
id: 1,
title: 'Earth'
}, ....]
}]
เราสามารถเก็บ childPlaces
แค่ childIds
แทนได้ และมี object ของสถานที่แยกเพื่อให้ง่ายต่อการแก้ไข หรือ ลบข้อมูล
initialTravelPlan = [{
id: 0,
title: '(Root)',
childIds: [1, 40, 47] ✅ // structure state in a flat way
}]
จากหลักการ 5 ข้อในการออกแบบ State Structure ของ React นั้น จะเห็นได้ว่าจริงๆแล้วมีหลายๆครั้งที่เราได้ออกแบบ State ให้มีความซับซ้อนเกินกว่าที่ควรจะเป็น และทำให้ระบบที่เราออกแบบนั้นยากต่อการแก้ไขและดูแลโดยไม่จำเป็น หลังจากนี้ลองกลับไปดู State Structure ของตัวเอง ถ้ามีส่วนที่ไหนยากเกินความจำเป็น ก็อย่าลืมหมั่น Refactor code กันอยู่เสมอนะครับ 😄
Reference