Frontend Development 11 min read

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.

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

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.

<code>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;</code>

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.

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

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.

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

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.

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

The usage in

App

becomes concise:

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

Further, the article demonstrates adding a static

Stage

component to render individual steps, allowing any number of stages with custom text and order.

<code>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>;
  }
}</code>

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

.

<code>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;</code>

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.

design patternsfrontend developmentReactCompound 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

login 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.