Skip to Main Content

Mastering Compound Components in React

Schematic drawing representing the structure of React's compound components.

In the vast landscape of React, compound components stand out as a powerful pattern, enabling developers to craft more expressive and flexible APIs. But what exactly are compound components, and how can you harness their potential in your applications? Let's dive in.

Understanding Compound Components

A compound component is a type of component that manages the internal state of a feature while delegating control of the rendering to the place of implementation as opposed to the point of declaration.

The idea is that you have two or more components that work together to accomplish a useful task. Typically one component is the parent, and the other is the child. The objective is to provide a more expressive and flexible API.

Another important aspect of this is the concept of "implicit state". The main component stores the state and shares it with its children so that they know how to render themselves based on that state.

A Practical Example

Consider the Toggle component, which acts as the main component, while On, Off, and Button are its child components. The children are added as static properties to the Toggle component, making the relationship explicit.

function App() { 
  return ( 
    <Toggle onToggle={on => console.log(on)}> 
      <Toggle.On>The button is on</Toggle.On> 
      <Toggle.Off>The button is off</Toggle.Off> 
      <Toggle.Button /> 
    </Toggle> 
  ) 
}

Behind the Scenes

The Toggle component manages its state and provides a mechanism to switch between states. It then maps its children to display in the order provided by the user, passing down the necessary state and functions.

class Toggle extends React.Component {
  /** 
   * Define static properties that will be used as children
   * Note that each property tracks `on` state
   * - On: when the toggle is switched to `on`, we display it's children 
   * - Off: when the toggle is switched to `off`, we display it's children
   * - Button: handles displaying the switch 
   */ 
  static On = ({ on, children }) => (on ? children : null) 
  static Off = ({ on, children }) => (on ? null : children) 
  static Button = ({ on, toggle, ...props }) => ( 
    <Switch on={on} onClick={toggle} {...props} /> 
  ) 

  state = { on: false } 

  // Switch the states and then execute the `onToggle` 
  // function passed down in the callback 
  toggle = () => 
    this.setState(
      ({ on }) => ({ on: !on }), 
      () => this.props.onToggle(this.state.on), 
    )

  // Maps children to display in the order provided by the user 
  render() { 
    return React.Children.map(this.props.children, childElement =>
      React.cloneElement(childElement, {
        on: this.state.on,
        toggle: this.toggle
      })
    ) 
  } 
}

Embracing Hooks

With the advent of React hooks, we can further refine our compound components. By using contexts and hooks like useState, useEffect, and useContext, we can make our components more concise and readable.

const ToggleContext = createContext();

function Toggle(props) {
  const [on, setOn] = useState(false);
  const toggle = useCallback(() => setOn(oldOn => !oldOn), []);
  const value = useMemo(() => ({ on, toggle }), [on]);

  return (
    <ToggleContext.Provider value={value}>
      {props.children}
    </ToggleContext.Provider>
  );
}

Conclusion

Compound components offer a robust way to design components that are both flexible and expressive. By understanding the underlying principles and leveraging the power of hooks, developers can create more maintainable and scalable React applications.

For a hands-on experience, check out this CodeSandbox example. For a deeper dive into compound components and advanced React patterns, these resources are invaluable:

Related Posts