skip to content
BlogZzz

[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 範圍與型別設計

參考

https://react.dev/learn/passing-data-deeply-with-context