patternjavascriptreactModerate
Children API — React.Children, cloneElement, and when to use context instead
Viewed 0 times
React.ChildrencloneElementcompound componentschildren APIcontext patternnamespace pattern
Problem
Compound components (Tabs, Accordion, Select) need to share state between a parent and its children (Tab, AccordionItem, Option) without explicit prop passing. Using React.Children.map and cloneElement to inject props is brittle — it only works for direct children and breaks with fragments or conditional rendering.
Solution
Prefer context over cloneElement for compound components:
// BRITTLE: cloneElement approach
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<div>
{React.Children.map(children, (child, i) =>
React.cloneElement(child, { active: i === activeIndex, onSelect: () => setActiveIndex(i) })
)}
</div>
);
}
// Breaks with: <Tabs><div><Tab /></div></Tabs> — Tab is not a direct child
// ROBUST: context approach
const TabsContext = createContext(null);
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
return (
<button
className={activeIndex === index ? 'active' : ''}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
Tabs.Tab = Tab; // attach as namespace
// Usage
<Tabs defaultIndex={0}>
<Tabs.Tab index={0}>Overview</Tabs.Tab>
<Tabs.Tab index={1}>Details</Tabs.Tab>
</Tabs>
// BRITTLE: cloneElement approach
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<div>
{React.Children.map(children, (child, i) =>
React.cloneElement(child, { active: i === activeIndex, onSelect: () => setActiveIndex(i) })
)}
</div>
);
}
// Breaks with: <Tabs><div><Tab /></div></Tabs> — Tab is not a direct child
// ROBUST: context approach
const TabsContext = createContext(null);
function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ index, children }) {
const { activeIndex, setActiveIndex } = useContext(TabsContext);
return (
<button
className={activeIndex === index ? 'active' : ''}
onClick={() => setActiveIndex(index)}
>
{children}
</button>
);
}
Tabs.Tab = Tab; // attach as namespace
// Usage
<Tabs defaultIndex={0}>
<Tabs.Tab index={0}>Overview</Tabs.Tab>
<Tabs.Tab index={1}>Details</Tabs.Tab>
</Tabs>
Why
React.Children.map only iterates direct children — wrapping children in a div or fragment breaks it. Context-based compound components work regardless of nesting depth, conditional rendering, or wrapper elements. The namespace pattern (Tabs.Tab) keeps the API discoverable.
Gotchas
- React.Children.count and React.Children.toArray include null/undefined children — filter them
- cloneElement merges props shallowly — if both parent and child define onClick, the child's is overwritten
- The context-based approach requires consumers to be inside the provider — document this requirement
- React.Children utilities are considered legacy — context is the recommended modern alternative
Code Snippets
Namespace compound component pattern
// Namespace compound component
function Accordion({ children }) { /* context provider */ }
function AccordionItem({ id, title, children }) { /* context consumer */ }
Accordion.Item = AccordionItem;
// Usage
<Accordion>
<Accordion.Item id="1" title="Section 1">Content</Accordion.Item>
<Accordion.Item id="2" title="Section 2">Content</Accordion.Item>
</Accordion>Context
When building compound component APIs where a parent coordinates state across multiple children
Revisions (0)
No revisions yet.