Web Totals

Compound Pattern

Compound Pattern

Create multiple components that work together to perform a single task


#Overview

With the Compound Pattern, we can create multiple components that work together to perform one single task.

Let's say for example that we have a Search input component. When a user clicks on the search input, we show a SearchPopup component that shows some popular locations.

To create this behavior, we can create a FlyOut compound component.

This FlyOut component is an example of a compound component, as it also exposes some sub-components that all work together to toggle and render the FlyOut component.

import React from 'react';
import { FlyOut } from './FlyOut';

export default function SearchInput() {
  return (
    <FlyOut>
      <FlyOut.Input placeholder='Enter an address, city, or ZIP code' />
      <FlyOut.List>
        <FlyOut.ListItem value='San Francisco, CA'>San Francisco, CA</FlyOut.ListItem>
        <FlyOut.ListItem value='Seattle, WA'>Seattle, WA</FlyOut.ListItem>
        <FlyOut.ListItem value='Austin, TX'>Austin, TX</FlyOut.ListItem>
        <FlyOut.ListItem value='Miami, FL'>Miami, FL</FlyOut.ListItem>
        <FlyOut.ListItem value='Boulder, CO'>Boulder, CO</FlyOut.ListItem>
      </FlyOut.List>
    </FlyOut>
  );
}
js

The FlyOut compound component is a stateful component - which means we don't have to add the stateful logic to the SearchInput component.


#Implementation

We can implement the Compound pattern using either a Provider, or React.Children.map.

#Provider

The FlyOut compound component consists of:

  • FlyoutContext to keep track of the visbility state of FlyOut
  • Input to toggle the FlyOut's List component's visibility
  • List to render the FlyOut's ListItemss
  • ListItem that gets rendered within the List.
const FlyOutContext = React.createContext();

export function FlyOut(props) {
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState('');
  const toggle = React.useCallback(() => setOpen((state) => !state), []);

  return (
    <FlyOutContext.Provider value={{ open, toggle, value, setValue }}>
      <div>{props.children}</div>
    </FlyOutContext.Provider>
  );
}

function Input(props) {
  const { value, toggle } = React.useContext(FlyOutContext);

  return <input onFocus={toggle} onBlur={toggle} value={value} {...props} />;
}

function List({ children }) {
  const { open } = React.useContext(FlyOutContext);

  return open && <ul>{children}</ul>;
}

function ListItem({ children, value }) {
  const { setValue } = React.useContext(FlyOutContext);

  return <li onMouseDown={() => setValue(value)}>{children}</li>;
}

FlyOut.Input = Input;
FlyOut.List = List;
FlyOut.ListItem = ListItem;
js

Although we didn't have to name our compound component's sub-components FlyOut.<ComponentName>, it's an easy way to identify compound components, and only requires a single import.


#React.Children.map

Another way to implement the Compound pattern, is to use React.Children.map in combination with React.cloneElement. Instead of having to use the Context API like in the previous example, we now have access to these two values through props.

export function FlyOut(props) {
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState('');
  const toggle = React.useCallback(() => setOpen((state) => !state), []);

  return (
    <div>
      {React.Children.map(props.children, (child) =>
        React.cloneElement(child, { open, toggle, value, setValue }),
      )}
    </div>
  );
}

function Input(props) {
  const { value, toggle } = React.useContext(FlyOutContext);

  return <input onFocus={toggle} onBlur={toggle} value={value} {...props} />;
}

function List({ children }) {
  const { open } = React.useContext(FlyOutContext);

  return open && <ul>{children}</ul>;
}

function ListItem({ children, value }) {
  const { setValue } = React.useContext(FlyOutContext);

  return <li onMouseDown={() => setValue(value)}>{children}</li>;
}

FlyOut.Input = Input;
FlyOut.List = List;
FlyOut.ListItem = ListItem;
js

All children components are cloned, and passed the value of open, toggle, value and setValue.

Stackblitz