Skip to main content

Component Patterns

Composition over Inheritance​

React ไม่สนับสนุนการสืบทอด component ด้วย extends (ต่าง class ทั่วไป) — ใช้ composition (ประกอบ component เข้าด้วยกันผ่าน children/props) แทน

function Card({ children }) {
return <div className="card">{children}</div>;
}

function ProfileCard() {
return (
<Card>
<Avatar />
<UserName />
</Card>
);
}
🧠 จำง่ายๆ

"React ประกอบ Lego ไม่ต่อสายพันธุ์" — แทนที่จะสร้าง SpecialCard extends Card (สืบทอด สายพันธุ์) React ใช้วิธี "เอาชิ้นส่วนมาประกอบกัน" (<Card><Avatar /></Card>) ยืดหยุ่นกว่า เพราะสลับชิ้นส่วนข้างในได้อิสระโดยไม่ต้องแก้ class แม่

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

เหตุผลที่ React team เลือก composition over inheritance ไม่ใช่แค่เรื่องสไตล์ — มันมาจาก ข้อจำกัดจริงของ inheritance ใน UI framework: ถ้า SpecialButton extends Button แล้ว React team แก้ internal implementation ของ Button ภายหลัง (เช่น เปลี่ยนจาก class component เป็น function component) โค้ดที่สืบทอดจาก Button ทั้งหมดจะพังทันที เพราะผูกติดกับ implementation detail ภายใน ส่วน composition ผูกแค่กับ "public interface" (props ที่รับ) ซึ่งเสถียรกว่ามาก — React core team ยืนยันในเอกสารทางการว่าหาสถานการณ์ที่ inheritance ให้ประโยชน์เหนือ composition ใน React ไม่เจอเลยตลอดหลายปีที่ดูแล framework นี้

Lifting State Up​

เมื่อ component สองตัวต้อง share state เดียวกัน ให้ย้าย state ขึ้นไปอยู่ที่ parent ร่วม (ไม่ใช่ copy state ไว้ 2 ที่แล้วพยายาม sync กัน)

function Parent() {
const [value, setValue] = useState('');
return (
<>
<Input value={value} onChange={setValue} />
<Preview text={value} />
</>
);
}
🧠 จำง่ายๆ

"State ที่ใช้ร่วมกัน ต้องมี 'เจ้าของบ้าน' คนเดียว" — ถ้าลูกสองคน (component) ต้องใช้ของชิ้น เดียวกัน ให้ของนั้นอยู่กับพ่อแม่ (parent) แล้วแจกให้ลูกยืมใช้ (props) ไม่ใช่ให้ลูกคนละคนถือของ คนละชิ้นแล้วพยายามทำให้เหมือนกันอยู่ตลอด (sync สอง state)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Lifting state up ที่ทำมากเกินไปในแอปใหญ่สร้างปัญหาที่เรียกว่า "prop drilling" — state ต้องถูกส่งผ่าน props ลงไปหลายชั้น component ที่ "ไม่ได้ใช้" state นั้นเลย แค่ทำหน้าที่ส่งต่อ (pass-through) ทำให้ component กลางทางผูกติดกับ shape ของ data ที่ตัวเองไม่เกี่ยวข้องโดยตรง วิธีแก้มาตรฐานคือ Context API (ย้าย state ไปอยู่ "กลางอากาศ" ที่ลูกหลานทุกชั้นเข้าถึงได้ ตรงๆ โดยไม่ต้องส่งผ่านมือทีละชั้น) แต่ Context เองก็มีข้อเสีย: component ที่ useContext จะ re-render ทุกครั้งที่ context value เปลี่ยน แม้จะใช้แค่บาง field ของ value นั้น — ทีมใหญ่จึงมักผสม local state (สำหรับ UI state เฉพาะจุด) + Context (สำหรับ state ที่ share กว้างจริงๆ) + state management library ภายนอก (Zustand, Redux ที่ optimize การ re-render ได้ดีกว่า Context ดิบๆ) ตามความเหมาะสมของแต่ละชั้นข้อมูล

Children, Render Props, Compound Components​

// Render props — ส่ง "วิธีเรนเดอร์" เป็น prop
<DataFetcher url="/api/user" render={(data) => <UserCard user={data} />} />

// Compound components — หลาย component ทำงานร่วมกันผ่าน implicit context
<Tabs>
<Tabs.List>
<Tabs.Tab>Profile</Tabs.Tab>
<Tabs.Tab>Settings</Tabs.Tab>
</Tabs.List>
<Tabs.Panels>
<Tabs.Panel>...</Tabs.Panel>
<Tabs.Panel>...</Tabs.Panel>
</Tabs.Panels>
</Tabs>
🧠 จำง่ายๆ

"Compound component คือรีโมททีวีกับทีวี — แยกกันแต่คุยกันผ่านสัญญาณที่มองไม่เห็น" — <Tabs.Tab> ไม่ได้รับ "ว่าตัวเองคือ tab ไหน" ผ่าน prop ตรงๆ แต่คุยกับ <Tabs> แม่ผ่าน Context ที่ซ่อนอยู่ข้างใน (มองจากข้างนอกดู "เป็นธรรมชาติ" เหมือน HTML <select><option>)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Render props pattern เคยเป็นวิธีมาตรฐานในการ "share stateful logic" ข้าม component ก่อนที่ hooks จะเกิดขึ้น (React 16.8) — ปัญหาของมันคือสร้าง "wrapper hell" เวลาต้อง share logic หลายตัวพร้อมกัน (<DataFetcher><Authenticated><ThemeProvider>... ซ้อนกันเป็นชั้นๆ) Custom hooks คือสิ่งที่เข้ามาแทนที่ render props สำหรับ use case นี้โดยเฉพาะ เพราะ share logic ได้โดยไม่ต้องเพิ่ม component ซ้อนใน tree เลย (const data = useDataFetcher(url) เรียกตรงๆ ไม่มี wrapper) — แต่ compound component pattern ยังไม่มีอะไรมาแทนที่ เพราะมันแก้ปัญหา คนละแบบ (ออกแบบ public API ที่อ่านง่ายเป็นธรรมชาติ ไม่ใช่แค่ share logic) ทีม React สมัยใหม่ ส่วนใหญ่เลยใช้ "custom hooks สำหรับ logic sharing" คู่กับ "compound components สำหรับ UI component library" ตามจุดประสงค์ที่ต่างกัน ไม่ใช่เลือกแบบใดแบบหนึ่งแทนกันทั้งหมด

English Summary

React favors composition over inheritance — component APIs stay stable because they depend only on props, not internal implementation details that could change. Lifting state up centralizes shared state in a common parent rather than syncing duplicated copies, but overdoing it causes prop drilling; Context solves that but re-renders every consumer on any value change, so large apps typically mix local state, Context, and external state libraries by scope. Render props were the pre-hooks way to share stateful logic but caused "wrapper hell" when composed — custom hooks replaced that use case cleanly, while compound components (implicit Context between siblings, e.g. <Tabs><Tabs.Tab>) remain the standard for building natural-feeling component library APIs, since they solve a different problem (API ergonomics, not logic sharing).