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"
/>
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>
Let's see How It's Implemented
- 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>
);
};
- 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>
);
}
- 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} />;
}
- 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}
/>
);
}
- 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>;
}
- 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} />;
}
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)