Null is not an ObjectHome Link
Compound Components

Compound Components

July 1, 2021 4 min read

Compound Components are where two or more components work together to accomplish a single task.

The components share some implicit state between them which resides in the parent component and passed down to the children.

Examples of this pattern:

<select>
  <option value="value1">Value One</option>
</select>

<AudioPlayer>
  <Play>Play song</Play>
  <Stop>Stop song</Stop>
</AudioPlayer>

<ol>
  <li>list item</li>
</ol>

Compound Components

Here's the Day&Night Compound Component I'll be using:

  • on renders Day
  • off renders Night
Good night

This Compound Component consists of several individual components:

<Toggle>
  <OnMessage>Good morning</OnMessage>
  <OffMessage>Good night</OffMessage>
  <Animation />
  <ToggleButton />
</Toggle>
  • The parent Toggle component holds the state for whether the toggle is on or off
  • This state is then shared to all the child components
  • The Child components then render the different UI's based on the shared state of the parent

The implementation of the child components:

// Accepts on prop from parent.  If on, render message, else render nothing
const OnMessage = ({ on, children }) => (on ? children : null);

// Accepts on prop from parent.  If on, render nothing else render message
const OffMessage = ({ on, children }) => (on ? null : children);

// Accepts on prop from parent.  If on, render day, else render night
const Animation = ({ on }) => <DayNightAnimation on={on} />;

// Accepts on and toggle props from parent.
// The toggle function prop allows us to toggle the state in the Parent Component
const ToggleButton = ({ on, toggle }) => <Switch on={on} onClick={toggle} />;

We need a way to pass the on and toggle props from the Toggle parent component to the child components.

Here's one approach to share state implicitly between a parent component and all its children:

const Toggle = ({ children }) => {
  const [on, setOn] = React.useState(false);
  const toggle = () => setOn(!on);
return React.Children.map(children, (child) =>
React.cloneElement(child, { on, toggle })
);
};

React.Children.map iterates over all the children, and maps them into new children, forwarding the props from the parent to the child components.

Flexible Compound Components

React.Children.map passes the props to all direct children of the parent component only.

If we add a wrapper div around the Animation component, it's now nested and no longer a direct child to the parent component.

The shared state is no longer available to the nested Animation component, instead the props are being passed to the div, causing the animation to no longer work.

<Toggle>
  <OnMessage>Good morning</OnMessage>
  <OffMessage>Good night</OffMessage>
<div className="wrapper">
<Animation />
</div>
<ToggleButton /> </Toggle>

To get the implicit shared state down from the parent <Toggle /> component to all child and grandchild components, we can use React.createContext.

Sharing state using React Context

Here's the updated parent <Toggle /> component, sharing state through context:

const ToggleContext = React.createContext();
ToggleContext.displayName = "ToggleContext";

function Toggle({ children }) {
  const [on, setOn] = React.useState(false);
  const toggle = () => setOn(!on);

  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      {children}
    </ToggleContext.Provider>
  );
}

We can consume the context value in our child components using a custom hook:

const useToggle = () => {
  const context = React.useContext(ToggleContext);
  if (context === undefined) {
    throw new Error("useToggle must be used within a <Toggle />");
  }
  return context;
};

Each child component can access state through the hook:

const Animation = () => {
const { on } = useToggle();
return ( <DayNightAnimation on={on} > ); }

The state is still implicitly shared between the <Toggle /> component and all its children. Only now, the state is accessible to all nested components too, no matter the depth.

Further Reading