Compound Components Pattern
Build flexible components that work together while maintaining clean APIs
Compound components allow you to create a set of components that work together to form a complete UI while giving consumers flexibility in how they compose them.
Implementation
1// Compound Components - Flexible component composition2import React, { createContext, useContext, useState, ReactNode } from 'react';34// Context for sharing state between compound components5interface AccordionContextType {6 openItems: Set<string>;7 toggleItem: (id: string) => void;8 allowMultiple: boolean;9}1011const AccordionContext = createContext<AccordionContextType | undefined>(undefined);1213// Custom hook to access accordion context14const useAccordionContext = () => {15 const context = useContext(AccordionContext);16 if (!context) {17 throw new Error('Accordion components must be used within an Accordion provider');18 }19 return context;20};2122// Main Accordion component23interface AccordionProps {24 children: ReactNode;25 allowMultiple?: boolean;26 defaultOpen?: string[];27}2829export const Accordion = ({30 children,31 allowMultiple = false,32 defaultOpen = []33}: AccordionProps) => {34 const [openItems, setOpenItems] = useState<Set<string>>(35 new Set(defaultOpen)36 );3738 const toggleItem = (id: string) => {39 setOpenItems(prev => {40 const newSet = new Set(prev);4142 if (newSet.has(id)) {43 newSet.delete(id);44 } else {45 if (!allowMultiple) {46 newSet.clear();47 }48 newSet.add(id);49 }5051 return newSet;52 });53 };5455 return (56 <AccordionContext.Provider value={{ openItems, toggleItem, allowMultiple }}>57 <div className="accordion">58 {children}59 </div>60 </AccordionContext.Provider>61 );62};6364// Accordion Item component65interface AccordionItemProps {66 children: ReactNode;67 id: string;68}6970Accordion.Item = ({ children, id }: AccordionItemProps) => {71 const { openItems } = useAccordionContext();72 const isOpen = openItems.has(id);7374 return (75 <div className="accordion-item" data-open={isOpen}>76 {React.Children.map(children, child => {77 if (React.isValidElement(child)) {78 return React.cloneElement(child, { id, isOpen } as any);79 }80 return child;81 })}82 </div>83 );84};8586// Accordion Header component87interface AccordionHeaderProps {88 children: ReactNode;89 id?: string;90 isOpen?: boolean;91}9293Accordion.Header = ({ children, id, isOpen }: AccordionHeaderProps) => {94 const { toggleItem } = useAccordionContext();9596 return (97 <button98 className="accordion-header"99 onClick={() => id && toggleItem(id)}100 aria-expanded={isOpen}101 >102 {children}103 <span className="accordion-icon">{isOpen ? '−' : '+'}</span>104 </button>105 );106};107108// Accordion Panel component109interface AccordionPanelProps {110 children: ReactNode;111 isOpen?: boolean;112}113114Accordion.Panel = ({ children, isOpen }: AccordionPanelProps) => {115 if (!isOpen) return null;116117 return (118 <div className="accordion-panel">119 {children}120 </div>121 );122};123124// Usage example125const FAQSection = () => {126 return (127 <Accordion allowMultiple defaultOpen={['q1']}>128 <Accordion.Item id="q1">129 <Accordion.Header>What is React?</Accordion.Header>130 <Accordion.Panel>131 React is a JavaScript library for building user interfaces.132 </Accordion.Panel>133 </Accordion.Item>134135 <Accordion.Item id="q2">136 <Accordion.Header>Why use compound components?</Accordion.Header>137 <Accordion.Panel>138 Compound components provide flexibility while maintaining a clean API.139 </Accordion.Panel>140 </Accordion.Item>141142 <Accordion.Item id="q3">143 <Accordion.Header>How do they work?</Accordion.Header>144 <Accordion.Panel>145 They share state through React Context and work together seamlessly.146 </Accordion.Panel>147 </Accordion.Item>148 </Accordion>149 );150};
Advanced Example
1// Advanced Compound Component - Tab System2interface TabsContextType {3 activeTab: string;4 setActiveTab: (id: string) => void;5 orientation: 'horizontal' | 'vertical';6}78const TabsContext = createContext<TabsContextType | undefined>(undefined);910export const Tabs = ({11 children,12 defaultTab,13 orientation = 'horizontal',14 onChange15}: {16 children: ReactNode;17 defaultTab?: string;18 orientation?: 'horizontal' | 'vertical';19 onChange?: (tabId: string) => void;20}) => {21 const [activeTab, setActiveTab] = useState(defaultTab || '');2223 const handleTabChange = (id: string) => {24 setActiveTab(id);25 onChange?.(id);26 };2728 return (29 <TabsContext.Provider value={{30 activeTab,31 setActiveTab: handleTabChange,32 orientation33 }}>34 <div className={`tabs tabs-${orientation}`}>35 {children}36 </div>37 </TabsContext.Provider>38 );39};4041Tabs.List = ({ children }: { children: ReactNode }) => {42 const { orientation } = useContext(TabsContext)!;4344 return (45 <div className={`tab-list tab-list-${orientation}`} role="tablist">46 {children}47 </div>48 );49};5051Tabs.Tab = ({52 children,53 id54}: {55 children: ReactNode;56 id: string;57}) => {58 const { activeTab, setActiveTab } = useContext(TabsContext)!;59 const isActive = activeTab === id;6061 return (62 <button63 className={`tab ${isActive ? 'active' : ''}`}64 onClick={() => setActiveTab(id)}65 role="tab"66 aria-selected={isActive}67 aria-controls={`panel-${id}`}68 >69 {children}70 </button>71 );72};7374Tabs.Panels = ({ children }: { children: ReactNode }) => {75 return <div className="tab-panels">{children}</div>;76};7778Tabs.Panel = ({79 children,80 id81}: {82 children: ReactNode;83 id: string;84}) => {85 const { activeTab } = useContext(TabsContext)!;86 const isActive = activeTab === id;8788 if (!isActive) return null;8990 return (91 <div92 className="tab-panel"93 role="tabpanel"94 id={`panel-${id}`}95 >96 {children}97 </div>98 );99};