Master Flexible React Components with Compound Component Patterns

This article demonstrates how to transform a basic Stepper component into a highly reusable and configurable React component using the compound component pattern, static properties, and React's Children utilities, while addressing common customization challenges and preparing for context API integration.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Master Flexible React Components with Compound Component Patterns

To celebrate the release of React 16.3, the author shares techniques that dramatically change how React components are built, enabling fully reusable components that can be flexibly used in many environments.

Using Compound Component Design Pattern

The initial Stepper component is simple but inflexible because its stage prop controls the progress and cannot be easily altered. The author lists typical customization questions such as moving the progress block, adding extra stages, changing stage content, or reordering stages.

import React, { Component } from 'react';
import Stepper from "./Stepper";
class App extends Component {
  render() {
    return (
      <Stepper stage={1} />
    );
  }
}
export default App;

To make the component flexible, the article introduces the compound component pattern. The Stepper class stores the current stage in state, provides a handleClick method to advance the stage, and renders a Progress and Steps component.

class Stepper extends Component {
  state = { stage: this.props.stage };
  static defaultProps = { stage: 1 };
  handleClick = () => {
    this.setState({ stage: this.state.stage + 1 });
  };
  render() {
    const { stage } = this.state;
    return (
      <div style={styles.container}>
        <Progress stage={stage} />
        <Steps handleClick={this.handleClick} stage={stage} />
      </div>
    );
  }
}
export default Stepper;

Instead of hard‑coding Progress and Steps inside Stepper, the article shows how to use props.children to insert them dynamically, allowing the parent to decide their order and placement.

const children = React.Children.map(this.props.children, child =>
  React.cloneElement(child, { stage, handleClick: this.handleClick })
);
return <div style={styles.container}>{children}</div>;

This approach gives the ability to render components on either side of the progress bar, but it introduces a new problem: the child components lose direct access to stage and handleClick. To solve this, the article uses React.Children.map and React.cloneElement to inject the needed props into each child.

const children = React.Children.map(this.props.children, child => {
  return React.cloneElement(child, { stage, handleClick: this.handleClick });
});

Static properties are then introduced to improve readability. By assigning Progress and Steps as static members of Stepper, they can be accessed as Stepper.Progress and Stepper.Steps without separate imports.

class Stepper extends Component {
  // ...component code
  static Progress = Progress;
  static Steps = Steps;
}

The usage in App becomes concise:

<Stepper stage={1}>
  <Stepper.Progress />
  <Stepper.Steps />
</Stepper>

Further, the article demonstrates adding a static Stage component to render individual steps, allowing any number of stages with custom text and order.

class Progress extends Component {
  render() {
    const { stage } = this.props;
    const children = React.Children.map(this.props.children, child =>
      React.cloneElement(child, { stage })
    );
    return <div style={styles.progressContainer}>{children}</div>;
  }
}

For animated step transitions, the Steps component wraps matching children in Transition components using ReactTransitionGroup. It only renders a child when its num matches the current stage.

class Steps extends Component {
  render() {
    const { stage, handleClick } = this.props;
    const children = React.Children.map(this.props.children, child => (
      stage === child.props.num && (
        <Transition appear={true} timeout={300} onEntering={entering} onExiting={exiting}>
          {child}
        </Transition>
      )
    ));
    return (
      <div style={styles.stagesContainer}>
        <div style={styles.stages}>
          <TransitionGroup>{children}</TransitionGroup>
        </div>
        <div style={styles.stageButton}>
          <Button disabled={stage===4} click={handleClick}>Continue</Button>
        </div>
      </div>
    );
  }
}
export default Steps;

An attempt to insert a title above Stepper.Steps breaks the component hierarchy because Steps is no longer a direct child of Stepper and thus cannot receive the stage prop.

The article emphasizes that React 16.3’s new Context API (still experimental at the time) will solve this limitation by allowing any component in the tree to access shared data, a topic that will be covered in part 2.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsReactCompound ComponentsContext APIReusable UI
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.