Higher-order Components
A Higher-order Component is a function, or component, that wraps around your regular React components allowing you create more reusable code. A HoC has the same lifecycle methods a regular React component, because under the hood, it is a regular React component.
Your HoC can have it’s own state, methods and props, which are then made available, via props, to the wrapped component(s). This comes in handy when you have a handful of components doing 90% of the same setup, rendering or actions.
Enough explanation, let’s get our feet wet
Let’s say you were tasked with creating an artboard, or canvas. You’re given an array of layers, each with different types, with the following criteria:
- Text layers should render out the text
- Image layers should render out the image
- Shapes should render out the shape
- Clicking on any layer type should select it
There’s a few ways you could tackle this. You could have a giant switch
statement that decided what to render based on layer type, but that could get ugly if you want to add complex interactions to specific layer types.
I think we’ll just create 3 separate components. Each component will take care of rendering out the layer and will also take care of selecting that layer.
Note: You will not be able to copy and paste the following code. We’re just going to focus on the overall idea of HoCs.
// TextLayer.jsx
class TextLayer extends React.Component {
handleClick() {
this.props.actions.selectLayer(this.props.layerId);
}
render() {
const styles = {
top: layer.y,
left: layer.x,
height: layer.height,
width: layer.width
};
return (
<div
className="layer text"
onClick={this.handleClick}>
{layer.text}
</div>
);
}
}
// ImageLayer.jsx
class ImageLayer extends React.Component {
handleClick() {
this.props.actions.selectLayer(this.props.layerId);
}
render() {
const styles = {
background: `url(${layer.url})`,
top: layer.y,
left: layer.x,
height: layer.height,
width: layer.width
};
return (
<div
className="layer image"
style={styles}
onClick={this.handleClick} />
);
}
}
// ShapeLayer.jsx
class ShapeLayer extends React.Component {
handleClick() {
this.props.actions.selectLayer(this.props.layerId);
}
render() {
const styles = {
backgroundColor: layer.backgroundColor,
top: layer.y,
left: layer.x,
height: layer.height,
width: layer.width,
border: `${layer.borderWidth} solid ${layer.borderColor}`
};
return (
<div
className="layer shape"
style={styles}
onClick={this.handleClick} />
);
}
}
So, as you can see we have 3 pretty simple components that basically do the same thing, with a few slight differences.
While each component renders differently, they’re all sized and positioned the same and they fire off the same action to select a layer. While this may not be the end of the world, it’ll get messy when you have to add n
more layer types in the future.
The old method of cleaning this up would be to write a Mixin
. While not deprecated yet, there’s talks of Mixins
going the way of the dinosaur, so we’re going to use the new method (called Composition) and create an HoC we can wrap these components with.
HoC Skeleton
export default ComposedComponent => {
class LayerBase extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<ComposedComponent
{...this.props}
{...this.state}
/>
);
}
}
return LayerBase;
};
Like we mentioned before, a HoC is just a React component, which you’ll notice when you take a look at the HoC skeleton.
We’re taking LayerBase
, which extends React.Component
, and passing it into a function called ComposedComponent
. Any component we wrap with LayerBase()
will now be passed our state, props and methods.
If we wanted to have ImageLayer
make use of our HoC skeleton, we could alter the ImageLayer
component to look something like the code below:
import LayerBase from "LayerBase.jsx";
class ImageLayer extends React.Component {
// ...
}
// This is the meat and potatoes.
//
// We're wrapping our ImageLayer component with LayerBase.
// Once we do this, ImageLayer has access to all the props
// we'll pass down from LayerBase
export LayerBase(ImageLayer);
When your ImageLayer
component is mounted, the LayerBase
HoC is going to render first, set up whatever state, props and methods are required and then mount your ImageLayer
component.
You can see that we spread the props and state into ComposedComponent
which passes them into ImageLayer
as regular props. A little later on you’ll see that we pass methods down exactly the same way.
So, let’s get some of that positioning code DRY’d up a bit. For now we’ll just tackle the ImageLayer
component, but this can be applied exactly the same way to our TextLayer
, ShapeLayer
, or any other component(s).
export default ComposedComponent => {
class LayerBase extends React.Component {
constructor(props) {
super(props);
// Setup our new method and bind it so we
// have access to this scope
this.getLayerPosition = this.getLayerPosition.bind(this);
}
getLayerPosition() {
// Return all the common positioning styles
return {
top: layer.y,
left: layer.x,
height: layer.height,
width: layer.width
};
}
render() {
return (
<ComposedComponent
{...this.props}
{...this.state}
getLayerPosition={this.getLayerPosition}
/>
);
}
}
// ...
};
We’ve added a new getLayerPosition()
method to return some common values and passed it down as a prop to our ImageLayer
.
We can now access the getLayerPosition()
just like you would any other prop method that was passed down to your component.
class ImageLayer extends React.Component {
// ...
render() {
// Grab our common positioning styles
const position = this.props.getLayerPosition();
// Merge the common styles with our layer type
// specific styles
const styles = Object.assign({}, position, {
background: `url(${layer.url})`
});
return (
<div
className="layer image"
style={styles}
onClick={this.onClick} />
);
}
}
Now that we’ve cleaned up the styling a bit, let’s use what we’ve learned and add our selection action to our HoC.
export default ComposedComponent => {
class LayerBase extends React.Component {
constructor(props) {
super(props);
this.getLayerPosition = this.getLayerPosition.bind();
this.handleClick = this.handleClick.bind();
}
getLayerPosition() {}
handleClick(layerId) {
this.props.actions.selectLayer(layerId);
}
render() {
return (
<ComposedComponent
{...this.props}
{...this.state}
getLayerPosition={this.getLayerPosition}
handleClick={this.handleClick}
/>
);
}
}
// ...
};
Once again, we’ve setup and bound our method and passed it down as a prop so that our ImageLayer
component has access to it.
class ImageLayer extends React.Component {
// ...
handleClick() {
// Pass the layerId up into out HoC so it
// can use it when we call our action
this.props.handleClick(this.props.layer.id);
// Having this method each of our components is strictly
// a preference. If you wanted to get away from having
// to create another method here, you can pass the handleClick
// prop directly to the onClick handler and then bind
// the layerId properly to pass it to the HOC.
//
// Example: onClick={this.props.handleClick.bind(this, this.props.layer.id)}
}
render() {
// Grab our common positioning styles
const position = this.props.getLayerPosition();
// Merge the common styles with our layer type
// specific styles
const styles = Object.assign({}, position, {
background: `url(${layer.url})`
});
return (
<div
className="layer image"
style={styles}
onClick={this.props.handleClick} />
);
}
}
We’ve changed our click handler a bit to offset the action call to our HoC. It’s a small change but it allows us to keep the components a little cleaner.
Fin
You’re probably thinking “We started with 3 components and ended up with 4, how is that making things simpler?!". While that is true, we’ve removed a bunch of unnecessary duplication that will allow us to quickly add new layer types without having to copy and paste properties over and over. We’ve also created smaller components that are easier to test.
I always like to make my components do one or two things really well. While that doesn’t always work out in the real world, it allows you to remove as much possibility of introducing bugs as you can.
Higher-order components seem a bit confusing at first, but when you boil it down, you’re still just working with components - Passing props down the chain, but now it allows you to keep your components small and lightweight, while creating a bit more re-usability.
Light Bed-Time Reading
We’ve just skimmed the surface of HoCs, but if you’d like to really dive in and see how they tick, here’s a couple good articles/gists:
- Higher-order Components by Sebastian Markbåge
- Mixins Are Dead. Long Live Composition by Dan Abramov