Skip to content

Understanding the Composition Pattern in React (Part 2)

If you’re relatively new to React, you’ve probably built components by passing lots of props to make them reusable. At first, this feels great — “Look, I made a configurable component!”

But as your project grows, the same component starts demanding more and more props just to support slightly different use cases. You end up with components that look like this:

<Card
title='Hello'
subtitle='World'
image={img}
badge='New'
variant='outlined'
size='large'
shadow='md'
onClick={handleClick}
disabled={isDisabled}
loading={isLoading}
icon='star'
/>

…and you quietly start hating your life.

This is the classic prop drilling + props explosion problem.

One of the most elegant ways to solve it in React is the composition pattern — instead of passing tons of props, you compose your UI by passing children and letting the parent component provide structure and behavior.

Let’s see this clearly with a very common UI pattern: an accordion.

The Classic Props-Based Accordion

Here’s a typical props-driven accordion implementation:

import { useState } from 'react';
export default function Accordion({ items }) {
const [activeIndex, setActiveIndex] = useState(null);
function toggle(index) {
setActiveIndex(activeIndex === index ? null : index);
}
return (
<div>
{items.map((item, index) => (
<div key={index} style={{ borderBottom: '1px solid #ccc' }}>
<button
onClick={() => toggle(index)}
style={{
width: '100%',
padding: '10px',
textAlign: 'left',
background: 'none',
border: 'none',
fontSize: '1rem',
cursor: 'pointer',
}}
>
{item.title}
</button>
{activeIndex === index && (
<div style={{ padding: '10px' }}>{item.content}</div>
)}
</div>
))}
</div>
);
}

Usage:

const accordionItems = [
{
title: 'What is React?',
content: 'React is a JavaScript library for building user interfaces.',
},
{
title: 'What is Next.js?',
content: 'Next.js is a full-stack React framework.',
},
{
title: 'What is Tailwind CSS?',
content: 'Tailwind is a utility-first CSS framework.',
},
];
<Accordion items={accordionItems} />;

This works… until you want:

  • Different header styles per item
  • Icons before/after titles
  • Different animations
  • Add extra elements (badges, subtitles, etc.)
  • Completely custom content layout

You either keep adding more props… or you give up and rewrite the component every time.

The Composition Pattern Approach

Now let’s rebuild the same accordion using composition:

import { useState, cloneElement, Children } from 'react';
function AccordionItem({
title,
children,
isOpen = false,
onToggle,
...props
}) {
return (
<div style={{ borderBottom: '1px solid #ccc' }} {...props}>
<button
onClick={onToggle}
style={{
width: '100%',
padding: '10px',
textAlign: 'left',
background: 'none',
border: 'none',
fontSize: '1rem',
cursor: 'pointer',
}}
>
{title}
</button>
{isOpen && <div style={{ padding: '0 10px 10px' }}>{children}</div>}
</div>
);
}
export default function Accordion({ children }) {
const [openIndex, setOpenIndex] = useState(null);
return (
<div>
{Children.map(children, (child, index) => {
// Only enhance AccordionItem children
if (child.type !== AccordionItem) return child;
return cloneElement(child, {
key: index,
isOpen: openIndex === index,
onToggle: () => setOpenIndex(openIndex === index ? null : index),
});
})}
</div>
);
}

Usage becomes very natural and flexible:

<Accordion>
<AccordionItem title='What is React?'>
React is a JavaScript library for building user interfaces.
</AccordionItem>
<AccordionItem title='What is composition?'>
Composition means building complex UIs by combining smaller, reusable
components rather than relying heavily on inheritance or massive prop lists.
</AccordionItem>
<AccordionItem title='Why not just use Context here?'>
<p>
For simple cases like an accordion, <code>cloneElement</code> +
composition is:
</p>
<ul>
<li>Lighter than Context</li>
<li>More explicit</li>
<li>Easier to reason about</li>
</ul>
</AccordionItem>
</Accordion>

Why This Feels So Much Better

  • You can put anything inside <AccordionItem> — paragraphs, lists, images, other components, etc.
  • You can add extra wrappers, icons, badges, or completely custom headers without changing AccordionItem
  • The API feels declarative and matches how HTML works (<details><summary>)
  • No props explosion — behavior comes from the container (Accordion), presentation from the children

Yes, the implementation has a few more lines (especially with cloneElement and Children.map), but the usage becomes dramatically more flexible and maintainable.

When to Use Composition vs Props

Use props-heavy approach when:

  • The component is very simple
  • Variations are limited and predictable
  • You want maximum type safety and autocomplete

Use composition when:

  • You want consumers to have maximum layout/content freedom
  • You’re building a UI library or design system
  • Future requirements are unknown or likely to grow
  • You want to avoid “props → 30” syndrome

Coming Up in Part 3

  • Compound components without cloneElement
  • Using React.Context the right way for composition
  • Real-world examples (Tabs, Dropdown, Modal, etc.)
  • Trade-offs: performance, TypeScript support, bundle size