React Curriculum

Compound Components Pattern with Context Sharing

Medium100% Free~30 mins#react#patterns#compound-components#context#design-systems#composition
Interview Answer (30-Second Summary)

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';
2
3// 1. Context for top-level state
4interface AccordionContextValue {
5 activeId: string | null;
6 toggle: (id: string) => void;
7}
8const AccordionContext = createContext<AccordionContextValue | null>(null);
9
10function useAccordion() {
11 const ctx = useContext(AccordionContext);
12 if (!ctx) throw new Error('Accordion compound subcomponents must be used within <Accordion>');
13 return ctx;
14}
15
16// 2. Item Context for scoped identity
17const 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}
23
24// 3. Root Component
25export 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));
28
29 return (
30 <AccordionContext.Provider value={{ activeId, toggle }}>
31 <div className="accordion-root">{children}</div>
32 </AccordionContext.Provider>
33 );
34}
35
36// 4. Subcomponents
37Accordion.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};
44
45Accordion.Header = function AccordionHeader({ children }: { children: React.ReactNode }) {
46 const { activeId, toggle } = useAccordion();
47 const id = useAccordionItem();
48 const isOpen = activeId === id;
49
50 return (
51 <button
52 onClick={() => toggle(id)}
53 aria-expanded={isOpen}
54 className="accordion-header"
55 >
56 {children}
57 <span>{isOpen ? '▲' : '▼'}</span>
58 </button>
59 );
60};
61
62Accordion.Panel = function AccordionPanel({ children }: { children: React.ReactNode }) {
63 const { activeId } = useAccordion();
64 const id = useAccordionItem();
65 if (activeId !== id) return null;
66
67 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:

html
<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:

  1. Implicit State Sharing via Context: Rather than passing isOpen and toggle down via props, subcomponents consume them from a scoped Context.
  2. Structural Freedom (Inversion of Control): The consumer can insert icons, tooltips, or wrapper <div>s anywhere between <Accordion.Item> and <Accordion.Header>.
  3. Context vs cloneElement: Older React code used React.Children.map with cloneElement. 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
What is the Compound Components pattern in React?

A pattern where a parent component and several child components collaborate to provide a unified UI element, sharing implicit state via React Context so consumers have full control over the rendered layout and markup.

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.

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.

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.

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.

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).