[Web] React Compound Component 設計模式與應用場景
/ 4 min read
Updated:Table of Contents
前言
很多 UI 元件不是單一節點,而是一組相關子元件,例如 Tabs、Select、Menu、Accordion
若全部包成單一元件,使用者會失去排列與擴充彈性
若全部拆成獨立元件,又會難以共享狀態
Compound Component 用父元件管理狀態,子元件專注呈現,讓 UI 同時保有語意與可組合性
核心概念
- 父元件提供狀態與行為,通常透過 Context
- 子元件只負責呈現,透過 Context 取用狀態
- 使用者可以自由安排子元件順序與結構
使用方式
使用方式常見如下
<Tabs defaultValue="pricing"> <Tabs.List> <Tabs.Trigger value="intro">介紹</Tabs.Trigger> <Tabs.Trigger value="pricing">價格</Tabs.Trigger> </Tabs.List> <Tabs.Panel value="intro">內容 A</Tabs.Panel> <Tabs.Panel value="pricing">內容 B</Tabs.Panel></Tabs>基本實作範例
以下是 Context + 子元件的基礎實作
import React, { createContext, useContext, useMemo, useState } from "react";
type TabsContextValue = { value: string; setValue: (value: string) => void;};
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() { const ctx = useContext(TabsContext); if (!ctx) throw new Error("Tabs.* must be used inside <Tabs>"); return ctx;}
type TabsProps = { defaultValue: string; children: React.ReactNode;};
export function Tabs({ defaultValue, children }: TabsProps) { const [value, setValue] = useState(defaultValue); const contextValue = useMemo(() => ({ value, setValue }), [value]);
return ( <TabsContext.Provider value={contextValue}> <div className="tabs">{children}</div> </TabsContext.Provider> );}
Tabs.List = function TabsList({ children }: { children: React.ReactNode }) { return <div className="tabs-list">{children}</div>;};
Tabs.Trigger = function TabsTrigger({ value, children,}: { value: string; children: React.ReactNode;}) { const { value: current, setValue } = useTabs(); return ( <button className={current === value ? "is-active" : ""} onClick={() => setValue(value)} type="button" > {children} </button> );};
Tabs.Panel = function TabsPanel({ value, children,}: { value: string; children: React.ReactNode;}) { const { value: current } = useTabs(); if (current !== value) return null; return <div className="tabs-panel">{children}</div>;};這樣的 API 讓使用者能自由調整排序與包裝層級,Tabs 內部仍維持一致的狀態管理
適用情境
Compound Component 常見於下列情境
- 複雜 UI 需要共享狀態,例如 Tabs、Dropdown、Menu、Accordion、Stepper
- 設計系統或元件庫,需要彈性組合但不暴露內部狀態
- 結構可變且視覺需可自訂,例如 Trigger 位置可調整或 Panel 需要額外容器
常見變體
元件庫常見變體如下
- Uncontrolled 由 Tabs 自行管理 value
- Controlled 由外部提供 value 與 onValueChange
- Slot 或 asChild 讓子元件決定實際 DOM 元素,需注意可存取性與事件綁定
需要注意的缺點
- Context 使用成本較高,需要理解 Provider 與 Consumer 的結構限制
- 子元件必須在 Provider 之下,否則會拋出錯誤
- 型別設計較複雜,TypeScript 需清楚定義 context 與 props
實務建議
- 對外提供清楚的子元件 API,例如 Tabs.List、Tabs.Trigger、Tabs.Panel
- 子元件讀不到 Context 時拋出明確錯誤
- 父元件負責狀態與邏輯,子元件只負責呈現
- 降低耦合,避免子元件直接操作 DOM 或持有過多商業邏輯
結論
- Compound Component 適合需要共享狀態又要保持組合彈性的 UI
- 父元件集中狀態與行為,子元件專注呈現,使用者仍可自由排列
- Context 是常見實作方式,但要注意 Provider 範圍與型別設計