Composition Pattern in React
Instead of drowning your components in props, use the composition pattern to build flexible, readable UIs that consumers actually enjoy using.
I want to show you a pattern that will change how you think about component design.
It's not new. It's not complicated. But most React developers I've worked with either don't know it exists, or reach for more props when they should be using it instead.
It's called the composition pattern — sometimes called the "donut pattern" — and it's one of the most effective tools for keeping components clean, flexible, and genuinely reusable.
The prop creep problem
Let's start with a real scenario. You're building a modal component. Simple enough. It needs a title, a close handler, and some content. So you write this:
type ConfiguredModalProps = {
isOpen: boolean;
onClose: () => void;
title?: string;
withDelimiter?: boolean;
withFooter?: boolean;
children: React.ReactNode;
headerActions?: () => React.ReactNode;
// ... more props
};
And the implementation:
export default function ConfiguredModal({
isOpen,
onClose,
headerActions,
title,
withDelimiter,
withFooter,
children,
}: ConfiguredModalProps) {
if (!isOpen) return null;
return (
<div aria-modal="true" role="dialog" onClick={onClose}>
<div onClick={(e) => e.stopPropagation()}>
<button onClick={onClose}>
<CircleX />
</button>
<div>
{title && <h2>{title}</h2>}
{headerActions ? headerActions() : null}
</div>
<div>{children}</div>
{withDelimiter && (
<span>
<hr />
</span>
)}
{withFooter && (
<footer>
<PrimaryButton onClick={() => console.log("Confirm clicked")}>
Confirm
</PrimaryButton>
</footer>
)}
</div>
</div>
);
}
Seven props, and already the component is hard to read. Every feature is gated behind a conditional. And on the consumer side, it looks like this:
<ConfiguredModal
title="My Modal Title"
isOpen={isOpen}
onClose={() => setIsOpen(false)}
withDelimiter
headerActions={() => (
<div>
<PrimaryButton variant="outlined" onClick={() => console.log("Edit clicked")}>
Edit
</PrimaryButton>
<PrimaryButton variant="outlined" onClick={() => console.log("Download clicked")}>
Download
</PrimaryButton>
</div>
)}
withFooter
>
<p>Some text..</p>
</ConfiguredModal>
This works. But it's tedious to write, hard to scan, and every new requirement means another prop. withCustomHeader? New prop. Footer with custom content? You'd need to change the interface again.
This is prop creep — the slow accumulation of configuration props that should have never existed in the first place.
The composition pattern
Instead of configuring a component through props, you give the consumer the building blocks and let them assemble what they need.
Here's what the modal looks like when redesigned with composition:
export default function ComposedModal({
isOpen,
onClose,
children,
}: ComposedModalProps) {
return (
isOpen && (
<div aria-modal="true" role="dialog" onClick={onClose}>
<div onClick={(e) => e.stopPropagation()}>
<button onClick={onClose}>
<CircleX />
</button>
{children}
</div>
</div>
)
);
}
Just the skeleton. No conditionals, no configuration logic, no withDelimiter anywhere. The modal does one thing: it renders when open and closes when dismissed.
Now we define its parts:
type HeaderProps = {
title: string;
children: React.ReactNode;
};
function Header({ title, children }: HeaderProps) {
return (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
function Body({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
function Footer({ children }: { children: React.ReactNode }) {
return (
<>
<span>
<hr />
</span>
<footer>{children}</footer>
</>
);
}
ComposedModal.Header = Header;
ComposedModal.Body = Body;
ComposedModal.Footer = Footer;
Each sub-component only defines the structure it owns. It doesn't know or care what goes inside it. That's the consumer's job.
And the consumer:
export default function ExamplePage() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<ComposedModal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<ComposedModal.Header title="My modal title">
<div>
<PrimaryButton variant="outlined" onClick={() => console.log("Edit clicked")}>
Edit
</PrimaryButton>
<PrimaryButton variant="outlined" onClick={() => console.log("Download clicked")}>
Download
</PrimaryButton>
</div>
</ComposedModal.Header>
<ComposedModal.Body>
<p>Some text...</p>
</ComposedModal.Body>
<ComposedModal.Footer>
<PrimaryButton onClick={() => console.log("Confirm clicked")}>
Confirm
</PrimaryButton>
</ComposedModal.Footer>
</ComposedModal>
</>
);
}
Look at how much cleaner this reads. The structure is self-documenting. You can see at a glance that there's a header, a body, and a footer. No boolean flags, no render props passed down, no guessing what withDelimiter does.
And if you don't need a footer? Just don't include ComposedModal.Footer. If you need a header without action buttons? Just don't put anything next to the title. The component doesn't need to know about your specific use case — it just provides the structure and gets out of the way.
Why this matters
The old ConfiguredModal tells the consumer: "Tell me what you need and I'll decide how to render it."
The ComposedModal tells the consumer: "Here's the structure. You decide what goes in it."
That's a fundamentally different relationship. The first is a vending machine. The second is a set of building blocks.
Composable components are easier to read, easier to extend, and don't require you to touch the component interface every time a new use case appears. The consumer gets full control over content without the component needing to grow.
When to use it
You should reach for composition whenever:
- Your component has multiple
with*boolean props that toggle layout sections - You have render props like
headerActions?: () => React.ReactNode - Different consumers need the same structure but different content in a specific region
- Your component is growing a new prop every time a new page uses it
If you look at a component and think "I just need to add one more prop and it'll be flexible enough" — that's the signal to stop and consider composition instead.
The pattern in the ecosystem
This isn't just a personal preference — it's how the most well-designed component libraries in the React ecosystem work. Radix UI, Headless UI, React Aria — all of them use composition as their primary API design principle. A Dialog has Dialog.Trigger, Dialog.Content, Dialog.Title, Dialog.Description. You assemble what you need.
Once you internalize this pattern, you'll start seeing opportunities for it everywhere. Cards with optional footers. Table rows with variable cell content. Sidebars with configurable sections. Any time you're about to add another prop to control layout, ask yourself: could this just be a slot?
This article is based on a chapter from my book Best Ways to Improve Your React Project — 2026 Edition — a guide for mid-to-senior React developers covering architecture, patterns, performance, React 19 APIs, testing, and more. If you found this useful, the book has 13 more chapters like it. Use code LAUNCH for a 40% launch discount.