วิธีออกแบบ State structure ที่ดีใน React

Komkanit
codesxdiary
Published in
3 min readMar 16, 2023

State structure ของ React ถูกออกแบบให้มีความยืดหยุ่นสูงสามารถทำได้หลายวิธีมาก เพื่อให้เหมาะสมกับระบบของเรา ซึ่งเป็นเรื่องที่สำคัญมากเรื่องนึงที่จะทำให้ระบบของเรานั้นซับซ้อนเกินความจำเป็นหรือไม่ ถ้าเราออกแบบอย่างไม่ถูกวิธี

ช่วงนี้กระแส alternative frontend framework กำลังมาแรง ผมก็อยากสวนกระแส ด้วยการลอง Relearn React ใหม่อีกครั้งเพื่อทบทวนสิ่งที่เคยเรียนรู้ทั้งหมด จึงได้มาเจอกับ document ของ React version Beta ที่กำลังพัฒนาอยู่ ซึ่ง Choosing the State Structure เป็นหนึ่งในหัวข้อนั้น หลังจากอ่านจบแล้วเลยอยากลองสรุปกับตัวเองอีกครั้งดู

ซึ่งหลักการสำคัญในการออกแบบ State Structure ที่ดีนั้น คือการออกแบบที่ทำให้ง่ายต่อการแก้ไข และ ลดโอกาสผิดพลาด และความซ้ำซ้อนให้ได้มากที่สุด ซึ่งมีหลักการคล้ายกับการ normalized Database Strcture ของ Database Engineer เลย โดยสามารถสรุปออกมาเป็น 5 ข้อด้วยกัน คือ

  1. 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

--

--