Project_CompoundComponents.jsx
React.js/lessons/20_advanced_patterns/projects/Project_CompoundComponents.jsx
/**
* Project: Tabs + Accordion 複合元件 — 進階設計模式練習
*
* 學習重點:
* - Compound Components 模式
* - 透過 Context 在子元件間共享狀態
* - 將子元件掛載到主元件(Tabs.List, Tabs.Content)
* - 靈活的 API 設計
*
* 使用方式:將此檔案內容複製到你的 App.jsx 中
*/
import { createContext, useContext, useState } from 'react';
// ============================================================
// Tabs 複合元件
// ============================================================
const TabsContext = createContext();
function Tabs({ children, defaultValue, onChange }) {
const [activeTab, setActiveTab] = useState(defaultValue);
const handleChange = (value) => {
setActiveTab(value);
onChange?.(value);
};
return (
<TabsContext.Provider value={{ activeTab, setActiveTab: handleChange }}>
<div style={{ marginBottom: '24px' }}>{children}</div>
</TabsContext.Provider>
);
}
function TabsList({ children }) {
return (
<div style={{
display: 'flex', gap: '4px', padding: '4px',
backgroundColor: '#f1f5f9', borderRadius: '10px', marginBottom: '16px',
}}>
{children}
</div>
);
}
function TabsTrigger({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
const isActive = activeTab === value;
return (
<button
onClick={() => setActiveTab(value)}
style={{
flex: 1, padding: '10px 16px', borderRadius: '8px', border: 'none',
backgroundColor: isActive ? 'white' : 'transparent',
color: isActive ? '#0f172a' : '#64748b',
fontWeight: isActive ? '600' : '400',
cursor: 'pointer', fontSize: '0.9rem',
boxShadow: isActive ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
transition: 'all 0.2s',
}}
>
{children}
</button>
);
}
function TabsContent({ value, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== value) return null;
return <div>{children}</div>;
}
Tabs.List = TabsList;
Tabs.Trigger = TabsTrigger;
Tabs.Content = TabsContent;
// ============================================================
// Accordion 複合元件
// ============================================================
const AccordionContext = createContext();
const AccordionItemContext = createContext();
function Accordion({ children, multiple = false }) {
const [openItems, setOpenItems] = useState(new Set());
const toggle = (id) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
if (!multiple) next.clear();
next.add(id);
}
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggle }}>
<div style={{
borderRadius: '12px', border: '1px solid #e2e8f0', overflow: 'hidden',
}}>
{children}
</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ value, children }) {
return (
<AccordionItemContext.Provider value={{ value }}>
<div style={{ borderBottom: '1px solid #e2e8f0' }}>{children}</div>
</AccordionItemContext.Provider>
);
}
function AccordionTrigger({ children }) {
const { openItems, toggle } = useContext(AccordionContext);
const { value } = useContext(AccordionItemContext);
const isOpen = openItems.has(value);
return (
<button
onClick={() => toggle(value)}
style={{
width: '100%', padding: '16px 20px', border: 'none',
backgroundColor: isOpen ? '#f8fafc' : 'white',
cursor: 'pointer', display: 'flex', justifyContent: 'space-between',
alignItems: 'center', fontSize: '0.95rem', fontWeight: '500',
color: '#0f172a', textAlign: 'left',
}}
>
{children}
<span style={{
transform: isOpen ? 'rotate(180deg)' : 'rotate(0)',
transition: 'transform 0.2s', color: '#94a3b8',
}}>
▼
</span>
</button>
);
}
function AccordionContent({ children }) {
const { openItems } = useContext(AccordionContext);
const { value } = useContext(AccordionItemContext);
if (!openItems.has(value)) return null;
return (
<div style={{
padding: '0 20px 16px', color: '#475569', fontSize: '0.9rem', lineHeight: 1.6,
backgroundColor: '#f8fafc',
}}>
{children}
</div>
);
}
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;
// ============================================================
// App — 展示這些複合元件
// ============================================================
function App() {
return (
<div style={{
maxWidth: '700px', margin: '32px auto', padding: '0 16px',
fontFamily: "'Segoe UI', 'Noto Sans TC', sans-serif",
}}>
<h1 style={{ color: '#0f172a', marginBottom: '4px' }}>Compound Components</h1>
<p style={{ color: '#94a3b8', marginTop: 0, marginBottom: '32px' }}>
Tabs 和 Accordion 複合元件設計模式展示
</p>
{/* Tabs 範例 */}
<h2 style={{ color: '#475569', fontSize: '1rem', marginBottom: '12px' }}>Tabs 元件</h2>
<Tabs defaultValue="overview" onChange={(v) => console.log('Tab changed:', v)}>
<Tabs.List>
<Tabs.Trigger value="overview">總覽</Tabs.Trigger>
<Tabs.Trigger value="features">功能</Tabs.Trigger>
<Tabs.Trigger value="pricing">價格</Tabs.Trigger>
<Tabs.Trigger value="faq">FAQ</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview">
<div style={{ padding: '20px', borderRadius: '12px', backgroundColor: '#f8fafc' }}>
<h3 style={{ margin: '0 0 8px', color: '#0f172a' }}>產品總覽</h3>
<p style={{ margin: 0, color: '#64748b', lineHeight: 1.6 }}>
這是一個使用 Compound Components 模式建構的 Tabs 元件。
父元件(Tabs)透過 Context 與子元件(TabsList、TabsTrigger、TabsContent)共享狀態,
使用者不需要手動傳遞 props,API 設計非常直覺。
</p>
</div>
</Tabs.Content>
<Tabs.Content value="features">
<div style={{ padding: '20px', borderRadius: '12px', backgroundColor: '#f8fafc' }}>
<h3 style={{ margin: '0 0 12px', color: '#0f172a' }}>功能列表</h3>
{['Context 共享狀態', '靈活的子元件組合', '受控 / 非受控模式', 'onChange 回呼'].map((f) => (
<div key={f} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 0' }}>
<span style={{ color: '#22c55e' }}>✓</span>
<span style={{ color: '#475569' }}>{f}</span>
</div>
))}
</div>
</Tabs.Content>
<Tabs.Content value="pricing">
<div style={{ padding: '20px', borderRadius: '12px', backgroundColor: '#f8fafc', textAlign: 'center' }}>
<h3 style={{ margin: '0 0 4px', color: '#0f172a' }}>免費開源</h3>
<p style={{ margin: 0, color: '#64748b' }}>這是教學範例,永久免費!</p>
</div>
</Tabs.Content>
<Tabs.Content value="faq">
<div style={{ padding: '20px', borderRadius: '12px', backgroundColor: '#f8fafc' }}>
<p style={{ margin: 0, color: '#64748b' }}>請參考下方的 Accordion FAQ 元件。</p>
</div>
</Tabs.Content>
</Tabs>
{/* Accordion 範例 */}
<h2 style={{ color: '#475569', fontSize: '1rem', margin: '32px 0 12px' }}>Accordion 元件(單選)</h2>
<Accordion>
<Accordion.Item value="q1">
<Accordion.Trigger>什麼是 Compound Components?</Accordion.Trigger>
<Accordion.Content>
<p>Compound Components 是一種 React 設計模式,多個元件透過 Context 隱式共享狀態。
類似 HTML 的 {'<select>'} 和 {'<option>'} 的關係。</p>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="q2">
<Accordion.Trigger>為什麼要使用這種模式?</Accordion.Trigger>
<Accordion.Content>
<p>它讓元件的 API 更加直覺和靈活。使用者可以自由組合子元件,
而不需要傳遞大量的 props。非常適合 UI 元件庫的設計。</p>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="q3">
<Accordion.Trigger>什麼時候該用自訂 Hook 而不是 Compound Components?</Accordion.Trigger>
<Accordion.Content>
<p>自訂 Hook 適合共享邏輯(不含 UI),Compound Components 適合共享帶有 UI 結構的元件。
如果你需要多個視覺元件協同工作,Compound Components 是更好的選擇。</p>
</Accordion.Content>
</Accordion.Item>
</Accordion>
{/* 多選 Accordion */}
<h2 style={{ color: '#475569', fontSize: '1rem', margin: '32px 0 12px' }}>Accordion 元件(多選)</h2>
<Accordion multiple>
<Accordion.Item value="step1">
<Accordion.Trigger>步驟 1:建立 Context</Accordion.Trigger>
<Accordion.Content>
<code style={{ display: 'block', padding: '12px', borderRadius: '8px', backgroundColor: '#0f172a', color: '#a5b4fc', fontSize: '0.8rem' }}>
{'const TabsContext = createContext();'}
</code>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="step2">
<Accordion.Trigger>步驟 2:建立 Provider(父元件)</Accordion.Trigger>
<Accordion.Content>
<p>父元件管理共享狀態,透過 Provider 傳遞給所有子元件。</p>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="step3">
<Accordion.Trigger>步驟 3:建立子元件</Accordion.Trigger>
<Accordion.Content>
<p>子元件用 useContext 取得共享狀態,實現各自的功能。</p>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="step4">
<Accordion.Trigger>步驟 4:掛載到主元件</Accordion.Trigger>
<Accordion.Content>
<code style={{ display: 'block', padding: '12px', borderRadius: '8px', backgroundColor: '#0f172a', color: '#a5b4fc', fontSize: '0.8rem' }}>
{'Tabs.List = TabsList;\nTabs.Trigger = TabsTrigger;\nTabs.Content = TabsContent;'}
</code>
</Accordion.Content>
</Accordion.Item>
</Accordion>
{/* API 展示 */}
<div style={{
marginTop: '32px', padding: '20px', borderRadius: '12px',
backgroundColor: '#0f172a', color: '#94a3b8', fontFamily: 'monospace',
fontSize: '0.8rem', lineHeight: 1.8,
}}>
<p style={{ color: '#818cf8', margin: '0 0 8px' }}>{'// 使用方式如此直覺:'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{'<Tabs defaultValue="tab1">'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{' <Tabs.List>'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{' <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{' <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{' </Tabs.List>'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{' <Tabs.Content value="tab1">內容 1</Tabs.Content>'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{' <Tabs.Content value="tab2">內容 2</Tabs.Content>'}</p>
<p style={{ color: '#f1f5f9', margin: 0 }}>{'</Tabs>'}</p>
</div>
</div>
);
}
export default App;
Articles liés
App.js
App.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.js).
Lire l'article →App.test.js
App.test.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.test.js).
Lire l'article →index.js
index.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/index.js).
Lire l'article →reportWebVitals.js
reportWebVitals.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/reportWebVitals.js).
Lire l'article →setupTests.js
setupTests.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/setupTests.js).
Lire l'article →Project_HelloReact.jsx
Project_HelloReact.jsx — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/projects/Project_HelloReact.jsx).
Lire l'article →