- React
- Advanced Component Design Patterns and Error Boundaries
- Compound Components Pattern with Context Sharing
Compound Components Pattern with Context Sharing
The Compound Components pattern allows multiple components to work together by implicitly sharing state through React Context, like native <select> and <option>. It provides an intuitive, expressive declarative API that inverts layout control to the consumer.
Key Learning Objectives
Design compound components that share state implicitly using React Context.
Provide flexible declarative APIs allowing consumers to customize component layout without prop explosion.
Prevent invalid usage with custom hook guards that check for enclosing compound providers.
Compare Context-based compound components with React.Children.map / cloneElement legacy patterns.
The Interview Problem & Context
Build a compound Accordion component featuring <Accordion>, <Accordion.Item>, <Accordion.Header>, and <Accordion.Panel> that shares active state cleanly without prop drilling.
1import React, { createContext, useContext, useState } from 'react';23// 1. Context for top-level state4interface AccordionContextValue {5 activeId: string | null;6 toggle: (id: string) => void;7}8const AccordionContext = createContext<AccordionContextValue | null>(null);910function useAccordion() {11 const ctx = useContext(AccordionContext);12 if (!ctx) throw new Error('Accordion compound subcomponents must be used within <Accordion>');13 return ctx;14}1516// 2. Item Context for scoped identity17const ItemContext = createContext<string | null>(null);18function useAccordionItem() {19 const id = useContext(ItemContext);20 if (!id) throw new Error('Accordion.Header and Panel must be used within <Accordion.Item>');21 return id;22}2324// 3. Root Component25export function Accordion({ children }: { children: React.ReactNode }) {26 const [activeId, setActiveId] = useState<string | null>(null);27 const toggle = (id: string) => setActiveId((prev) => (prev === id ? null : id));2829 return (30 <AccordionContext.Provider value={{ activeId, toggle }}>31 <div className="accordion-root">{children}</div>32 </AccordionContext.Provider>33 );34}3536// 4. Subcomponents37Accordion.Item = function AccordionItem({ id, children }: { id: string; children: React.ReactNode }) {38 return (39 <ItemContext.Provider value={id}>40 <div className="accordion-item">{children}</div>41 </ItemContext.Provider>42 );43};4445Accordion.Header = function AccordionHeader({ children }: { children: React.ReactNode }) {46 const { activeId, toggle } = useAccordion();47 const id = useAccordionItem();48 const isOpen = activeId === id;4950 return (51 <button52 onClick={() => toggle(id)}53 aria-expanded={isOpen}54 className="accordion-header"55 >56 {children}57 <span>{isOpen ? '▲' : '▼'}</span>58 </button>59 );60};6162Accordion.Panel = function AccordionPanel({ children }: { children: React.ReactNode }) {63 const { activeId } = useAccordion();64 const id = useAccordionItem();65 if (activeId !== id) return null;6667 return <div className="accordion-panel">{children}</div>;68};
Deep Architectural Explanation
Anatomy of Compound Components
Native HTML gives us the gold standard of compound components:
<select name="fruit">
<option value="apple">Apple</option>
<option value="orange">Orange</option>
</select>The <select> manages the active value, while <option> components configure the choices. Neither requires verbose options={[...]} props or rigid structural constraints.
Key Architectural Principles:
- Implicit State Sharing via Context: Rather than passing
isOpenandtoggledown via props, subcomponents consume them from a scoped Context. - Structural Freedom (Inversion of Control): The consumer can insert icons, tooltips, or wrapper
<div>s anywhere between<Accordion.Item>and<Accordion.Header>. - Context vs cloneElement: Older React code used
React.Children.mapwithcloneElement. This was brittle because it only worked for direct children—wrapping a child in a<div>broke the component! Context works arbitrarily deep in the subtree.
Common Interview Traps & Pitfalls
Using React.Children.map with cloneElement, which breaks when consumers add wrapper divs.
Failing to throw meaningful errors when subcomponents are rendered outside the parent provider.
Passing props down through 4 layers of components instead of using scoped Context.
Senior Interviewer Follow-Up Probes
How does Radix UI and Headless UI build compound components with full ARIA accessibility?
How can you support both controlled (value/onChange) and uncontrolled (defaultValue) modes in compound components?
What are the trade-offs between static property attachment (Accordion.Header) vs individual named exports?
Frequently Asked Interview Questions
Why avoid React.Children.map and React.cloneElement today?
cloneElement only injects props into immediate children. If a consumer wraps a subcomponent in a div, styling wrapper, or transition component, prop injection fails. Context traverses down the component tree regardless of depth.
How do compound components improve developer experience (DX)?
They eliminate gigantic configuration props (like 15 different props for header rendering, footer rendering, icons, etc.) and allow consumers to assemble the UI naturally like HTML tags.
How do you guard against compound subcomponents used outside the parent?
Create a dedicated custom hook (e.g., useAccordion) that reads the context and immediately throws a descriptive Error if the context value is null or undefined.
Can compound components support keyboard accessibility?
Yes. Compound components typically store a ref collection or active index in Context, allowing subcomponents to handle ArrowUp, ArrowDown, Home, and End keys according to WAI-ARIA authoring practices.
How do compound components handle uncontrolled vs controlled state?
The parent component checks if an activeId prop was passed. If passed, it uses that prop (controlled); otherwise, it falls back to internal useState (uncontrolled).
