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.
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
Steppercomponent is simple but inflexible because its
stageprop 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
Stepperclass stores the current
stagein state, provides a
handleClickmethod to advance the stage, and renders a
Progressand
Stepscomponent.
<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
Progressand
Stepsinside
Stepper, the article shows how to use
props.childrento 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
stageand
handleClick. To solve this, the article uses
React.Children.mapand
React.cloneElementto 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
Progressand
Stepsas static members of
Stepper, they can be accessed as
Stepper.Progressand
Stepper.Stepswithout separate imports.
<code>class Stepper extends Component {
// ...component code
static Progress = Progress;
static Steps = Steps;
}</code>The usage in
Appbecomes concise:
<code><Stepper stage={1}>
<Stepper.Progress />
<Stepper.Steps />
</Stepper></code>Further, the article demonstrates adding a static
Stagecomponent 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
Stepscomponent wraps matching children in
Transitioncomponents using
ReactTransitionGroup. It only renders a child when its
nummatches 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.Stepsbreaks the component hierarchy because
Stepsis no longer a direct child of
Stepperand thus cannot receive the
stageprop.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.