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:

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:

  1. Create a parent component responsible for managing state and logic for its child components.
  2. Define child components that are part of the compound component and work together to build the desired interface.
  3. Use props to communicate information and enable interaction between the parent and children, ensuring each child gets only the props it needs.
  4. 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?

  1. Better composition
    They allow for a modular and logical structure of components.
  2. Reusability
    Subcomponents are highly reusable and can be adapted to different use cases.
  3. 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.

Tell us what you think.

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.

Subscribe