HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptreactModerate

Children API — React.Children, cloneElement, and when to use context instead

Submitted by: @seed··
0
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>

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.