DEV Community

Cover image for React Advanced Patterns: Learn About Compound Components
CodeShipper
CodeShipper

Posted on

React Advanced Patterns: Learn About Compound Components

React Compound Components

In this post, we will learn what are React Compound Components, their advantages and how to actually build them.

What are React Compound Components

React Compound Components is a design pattern that allows the creation of components that work together in a cohesive way while maintaining flexibility and separation of concerns. Instead of passing a large number of props to a single component, this pattern allows for the use of multiple components that are tightly coupled but separated into smaller, reusable parts. This enhances both readability and customization by allowing the consumer to compose the desired UI without worrying about how it works internally.

Advantages of Compound Components

  • Improved Readability: Instead of a component with numerous props, compound components allow the structure and relationships between parts to be visually evident in JSX.
  • Better Flexibility: Compound components enable more flexible customization. Users can arrange the subcomponents in any way they see fit without modifying the parent component's implementation.
  • Encapsulation and Separation of Concerns: Each subcomponent is responsible for its behavior, allowing cleaner and more maintainable code.
  • Enhanced Composition: Consumers can compose the UI naturally by nesting subcomponents, leading to more intuitive API usage.

Example: Stepper Component

Let's compare the compound component pattern with the traditional prop-based pattern using a Stepper component example.

1. Traditional Prop-Based Approach

    <Stepper
      steps={[
        { label: "Step 1", content: "This is step 1" },
        { label: "Step 2", content: "This is step 2" },
        { label: "Step 3", content: "This is step 3" }
      ]}
      activeStep={1}
      orientation="horizontal"
    />
Enter fullscreen mode Exit fullscreen mode
Disadvantages:
  • Limited flexibility: Customization options are restricted to the props exposed by the Stepper component. Adding more props or modifying structure requires changes to the Stepper component itself.
  • Tight coupling: The Stepper component handles too much logic, reducing separation of concerns.

2. Compound Component Approach

    <Stepper index={index} backgroundColor="lightgreen">
       {steps.map((step, index) => (
         <Stepper.Step key={index}>
            <Stepper.StepIndicator>
              <Stepper.StepStatus complete={""} />
              </Stepper.StepIndicator>
              <Stepper.StepTitle>{step.title}</Stepper.StepTitle>
              <Stepper.StepSeparator />
          </Stepper.Step>
        ))}
     </Stepper>
Enter fullscreen mode Exit fullscreen mode

Let's see How It's Implemented

  1. First, you need a context to provide the state from top level Stepper component to the child components, so let's create a stepper context:

    import React from "react";

    const stepperContext = React.createContext();

    export const useStepper = () => {
      const context = React.useContext(stepperContext);
      if (context === undefined) {
        throw new Error(
          "Context is undefined. Did you forget to wrap inside the provider"
        );
      }
      return context;
    };

    export const Provider = ({ children, value }) => {
      return (
        <stepperContext.Provider value={value}>{children}</stepperContext.Provider>
      );
    };
Enter fullscreen mode Exit fullscreen mode
  1. Next, let's build the top level Stepper component:
    import React, { Children } from "react";

    import { Provider } from "./stepper-context";

    export default function Stepper({
      index,
      children,
      backgroundColor,
      foregroundColor,
    }) {
      const elements = Children.toArray(children);

      function getStatus(stepIndex) {
        if (stepIndex < index) return "complete";
        if (stepIndex > index) return "incomplete";
        return "active";
      }

      return (
        <div style={styles.stepper}>
          {elements.map((element, index) => {
            return (
              <Provider
                key={index}
                value={{
                  index,
                  totalSteps: elements.length,
                  isLastStep: index === elements.length - 1,
                  status: getStatus(index),
                  backgroundColor,
                  foregroundColor,
                }}
              >
                {element}
              </Provider>
            );
          })}
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode
  1. Next, let's create a Step Component, that one is pretty simple:
    import React from "react";

    export default function Step(props) {
      return <div style={styles.step} {...props} />;
    }
Enter fullscreen mode Exit fullscreen mode
  1. Next is the StepIndicator Component, this component will dinamically display the steps colors base on their completion or active status:
    import React from "react";
    import { useStepper } from "./stepper-context";
    import styles from "./styles";

    export default function StepIndicator(props) {
      const { status, backgroundColor, foregroundColor } = useStepper();
      const isActive = status === "active";
      const isCompleted = status === "complete";

      const getBorderColor = () => {
        const color = backgroundColor || "dodgerblue";
        if (isActive) return "1px solid " + color;
        if (!isCompleted) return "1px solid gray";
        return "none";
      };

      return (
        <div
          style={styles.stepIndicator({
            isCompleted,
            isActive,
            getBorderColor,
            backgroundColor,
            foregroundColor
          })}
          {...props}
        />
      );
    }
Enter fullscreen mode Exit fullscreen mode
  1. Next is the StepStatus Component, this one will displat the steps numbers or custom component base on step status if provided:
    import React from "react";
    import { useStepper } from "./stepper-context";

    export default function StepStatus({ complete, incomplete, active }) {
      const { status, index } = useStepper();
      const isActive = status === "active";
      const isCompleted = status === "complete";

      if (isActive) {
        return active || index + 1;
      }

      if (isCompleted) {
        return complete || index + 1;
      }

      return <div>{incomplete || index + 1}</div>;
    }
Enter fullscreen mode Exit fullscreen mode
  1. Finally the StepSeparator, and StepTitle component
    import React from "react";
    import { useStepper } from "./stepper-context";
    import styles from "./styles";

    export default function StepSeparator() {
      const { backgroundColor, status, isLastStep } = useStepper();
      const isCompleted = status === "complete";

      if (isLastStep) return null;

      const getBorderColor = () => {
        const color = backgroundColor || "dodgerblue";
        if (isCompleted) return "1px solid " + color;
        return "1px solid gray";
      };

      return <div style={styles.stepSeparator({ getBorderColor })} />;
    }

    import React from "react";

    export default function StepTitle(props) {
      return <p {...props} />;
    }
Enter fullscreen mode Exit fullscreen mode

By putting it all together, we have this nice composable Compound Stepper component that we can easily reuse and extend.

If you like these type of contents, you can follow here to see more of them.

You can also get the full working codebase here, with the styling to take a closer look or use in your project.

Happy Coding!

Top comments (0)