What is Component Composition?
At its core, component composition is about combining smaller components to build more complex UIs. Instead of creating a massive component with tons of conditional logic, you compose your UI from simple, focused components that can be reused across your app.
Think of it as LEGO blocks: each small block does one thing, and you can combine them in multiple ways to create something bigger.
Why Composition Over Inheritance?
In React, composition is favored over inheritance for several reasons:
- Flexibility: You can mix and match components without being tied to a rigid hierarchy.
- Reusability: Smaller components can be used in multiple contexts.
- Maintainability: Changes in one component won’t ripple through unrelated parts of your app.
- Declarative: React’s declarative style aligns naturally with composition.
Example: A Simple Accordion
Imagine you want to build an accordion. A naïve approach might involve a single component managing all the state and rendering. But with composition, you can separate concerns:
// Accordion.js
export function Accordion({ children }) {
const [openIndex, setOpenIndex] = React.useState(null);
return React.Children.map(children, (child, index) =>
React.cloneElement(child, {
isOpen: index === openIndex,
onToggle: () => setOpenIndex(index === openIndex ? null : index),
})
);
}
// AccordionItem.js
export function AccordionItem({ title, children, isOpen, onToggle }) {
return (
<div className="accordion-item">
<button onClick={onToggle}>{title}</button>
{isOpen && <div className="accordion-content">{children}</div>}
</div>
);
}
// Usage
<Accordion>
<AccordionItem title="Section 1">Content 1</AccordionItem>
<AccordionItem title="Section 2">Content 2</AccordionItem>
<AccordionItem title="Section 3">Content 3</AccordionItem>
</Accordion>
Here, the Accordion component handles state, but each AccordionItem is responsible only for rendering itself. This separation makes both components highly reusable.
Leveraging React Context for Composition
Sometimes, passing props down multiple levels becomes cumbersome. That’s where React Context comes in. It allows you to share state and behavior across components without explicit prop drilling.
const AccordionContext = React.createContext();
export function Accordion({ children }) {
const [openIndex, setOpenIndex] = React.useState(null);
return (
<AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
{children}
</AccordionContext.Provider>
);
}
export function AccordionItem({ title, children, index }) {
const { openIndex, setOpenIndex } = React.useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<div className="accordion-item">
<button onClick={() => setOpenIndex(isOpen ? null : index)}>{title}</button>
{isOpen && <div className="accordion-content">{children}</div>}
</div>
);
}
With this approach:
AccordionItemdoesn’t need to receiveisOpenoronToggleas props.- Adding new items or nesting accordions becomes trivial.
- The code remains clean, maintainable, and declarative.
When to Use Component Composition
- Building reusable UI libraries.
- Handling complex layouts with multiple child components.
- When prop drilling becomes messy.
- Implementing patterns like tabs, modals, accordions, or tooltips.
Takeaways
- Composition is more powerful than inheritance in React.
- Break your UI into small, focused components.
- Use React Context when passing props through multiple layers.
- Focus on reusability, flexibility, and maintainability.
Component composition is not just a pattern; it’s a mindset. Once you start thinking in terms of composable pieces, your React codebase becomes cleaner, easier to maintain, and a joy to work with.