One of the main challenges we face when developing large applications with React is how to pass information from parent to child components, especially when dealing with deeply nested component trees. Typically, this is solved using “contexts” or “stores” such as Redux, Zustand…
But with React, there’s another way to inherit those properties more simply, using less code and creating more efficient applications: Compound Components — an advanced React pattern that improves component composition and property inheritance.
The dreaded Prop Drilling
Chances are, you've encountered a scenario where a component slowly accumulates more and more props just to ensure its deeply nested child components work properly. This pattern, known as Prop Drilling, occurs when props are passed down through several layers of components, even when some of those components don’t need them directly.
Why should we avoid it? Here are the main drawbacks:
- The code becomes harder to maintain: if a prop changes, you may have to modify many components that don’t even use it.
- It causes unnecessary coupling: intermediate components are forced to handle props they don’t care about.
- Limited scalability: as your app grows, this pattern becomes unmanageable.
- Reduced reusability: it’s harder to reuse components that depend on irrelevant props.
Implementing Compound Components
To avoid this, we can use Compound Components, where components are structured into logical building blocks with specific responsibilities, sharing a common state through a single parent component. This significantly reduces and simplifies our code. Here's how to implement it step by step:
- Create a parent component responsible for managing state and logic for its child components.
- Define child components that are part of the compound component and work together to build the desired interface.
- Use props to communicate information and enable interaction between the parent and children, ensuring each child gets only the props it needs.
- Render the child components from within the parent, ensuring proper integration and shared behavior.
Implementation example
Let’s walk through an example of implementing a configurable header using Compound Components in React.
We’ll start with a component that receives many props and passes them directly to its children.
<Header
actions={headerData?.actions}
logoImg={headerData?.logoImg}
logoSize={headerData?.logoSize}
dataTestId="headerTest"
handleClose={headerData?.handleClose}
handleMenuClick={toggle}
hasClose={headerData?.hasClose}
hasLogo={headerData?.hasLogo}
hasMenu={headerData?.hasMenu}
information={headerData?.information} />
Let's modify it by creating logical blocks within the component:
<Header dataTestId="headerTest">
<Header.Config
logoImg={headerData?.logoImg}
logoSize={headerData?.logoSize}
handleMenuClick={toggle}
hasLogo={headerData?.hasLogo}
hasMenu={headerData?.hasMenu}
/>
<Header.Information information={headerData?.information} />
<Header.Actions actions={headerData?.actions} />
<Header.Close handleClose={headerData?.handleClose} hasClose={headerData?.hasClose} />
</Header>
Now let's go to the header.tsx file, where we have those same blocks with their functionality, each receiving only the props they need:
export const Header = ({ dataTestId, children }) => {
return (
<header data-testid={dataTestId}>
{children}
</header>
);
};
Header.Config = function HeaderConfig({
logoImg = "default",
logoSize = 50,
handleMenuClick,
hasLogo = true,
hasMenu = true,
}) {
return (
<div >
{hasMenu && (
<div onClick={handleMenuClick}>
<BurguerIcon />
</div>
)}
{hasLogo && (
<div onClick={handleMenuClick}>
<LogoIcon size={logoSize} logoImg={logoImg} />
</div>
)}
</div>
);
};
Header.Information = function HeaderInformation({ information }) {
return (
<div >
<HeaderInformation information={information} />
</div>
);
};
Header.Actions = function HeaderActions({ actions }) {
return (
<div>
<HeaderActionGroup items={actions?.items} renderer={actions?.renderer} />
</div>
);
};
Header.Close = function HeaderClose({ hasClose = false, handleClose }) {
return (
hasClose && (
<div onClick={handleClose}>
<CloseIcon />
</div>
)
);
};
What are the advantages of Compound Components?
- Better composition
They allow for a modular and logical structure of components. - Reusability
Subcomponents are highly reusable and can be adapted to different use cases. - Single responsibility
Each subcomponent handles a specific task, which improves maintainability.
Conclusion
Compound Components are a very useful component composition pattern for building complex interfaces in a more visual and intuitive way. They help avoid Prop Drilling by better organizing communication between components without the need to pass unnecessary props through multiple levels. By using this approach, we can write cleaner, more modular, and maintainable code, making it easier to scale React applications.
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.