Introduction
Since the React core team dropped the support for mixins, which were the standard way of sharing code between components, the use of composition over inheritance has become the right way for building user interfaces.
A High Order Component (HOC from now on) is a pattern for reusing component logic that has evolved to maturity. It’s used throughout many famous libraries like Redux or React-Router, and it’s a direct consequence of React’s compositional nature.
In practical terms a HOC is nothing more that a function that takes an input component and returns an enhanced/modified version of that component. Sometimes they are also called decorators.
The problem
At work we have a CMS that is also an ecommerce platform. We needed to implement a few statistic widgets for the dashboard, showing for example the number of orders or the number of abandoned carts. The idea was to build isolated standalone components, each one with its own state and its own logic for calling the API endpoints.
We noticed soon enough that the majority of these components needed a few inputs for filtering the data on the backend. We really didn’t want to show a chart displaying the orders of the last 3 years, so we decided to include a range datepicker in most of our components. We used the wonderful react-day-picker library for this purpose.
Implementing the range datepicker logic for a single component was then straightforward (most of the code is taken from the react-day-picker docs)
import React from 'react';
import Loading from './Loading';
import DayPickerInput from 'react-day-picker/DayPickerInput';
import 'react-day-picker/lib/style.css';
class OrdersIndex extends React.Component {
constructor(props) {
super(props);
this.handleFromChange = this.handleFromChange.bind(this);
this.handleToChange = this.handleToChange.bind(this);
this.state = {
loading: false,
from: undefined,
to: undefined,
count: null,
orders: null
};
}
componentWillUnmount() {
clearTimeout(this.timeout);
}
focusTo() {
this.timeout = setTimeout(() => this.to.getInput().focus(), 0);
}
showFromMonth() {
const { from, to } = this.state;
if (!from) return;
if (moment(to).diff(moment(from), 'months') < 2) {
this.to.getDayPicker().showMonth(from);
}
}
handleFromChange(from) {
this.setState({ from }, () => {
if (!this.state.to) {
this.focusTo();
}
});
}
handleToChange(to) {
this.setState({ to }, () => {
this.showFromMonth();
});
}
fetch(params = {}) {
this.setState({
loading: true
});
// fetch data from backend, using params for filtering
}
renderChart(orders) {
// render chart
}
render() {
const { loading, count, orders, from, to } = this.state;
const modifiers = { start: from, end: to };
return (
<div>
<DayPickerInput
value={from}
placeholder={trans('common.from')}
format="LL"
dayPickerProps={{
selectedDays: [from, { from, to }],
disabledDays: { after: to },
toMonth: to,
modifiers,
numberOfMonths: 1,
}}
onDayChange={this.handleFromChange}
/>{' '}
—{' '}
<DayPickerInput
ref={el => (this.to = el)}
value={to}
placeholder={trans('common.to')}
format="LL"
dayPickerProps={{
selectedDays: [from, { from, to }],
disabledDays: { before: from },
modifiers,
month: from,
fromMonth: from,
numberOfMonths: 1,
}}
onDayChange={this.handleToChange}
/>
{loading
? <Loading />
: <div>
<h3><i className="fa fa-shopping-cart"></i> {count}</h3>
{this.renderChart(orders)}
</div>
}
</div>
);
}
}
It was immediately clear that we needed to find a way to share the logic of the range datepicker between our components, otherwise we would have to duplicate a lot of code.
HOC to the rescue
Whenever you find yourself copying and pasting the same code in different components, in most of the cases it’s a signal that such code can be hoisted up in a HOC.
We created a new HOC, called WithDateRange
to extract the daterange functionality from existing components.
import React from 'react';
import DayPickerInput from 'react-day-picker/DayPickerInput';
import 'react-day-picker/lib/style.css';
module.exports = function (WrappedComponent) {
// WrappedComponent is our original statistic component
return class WithDateRange extends React.Component {
constructor(props) {
super(props);
this.handleFromChange = this.handleFromChange.bind(this);
this.handleToChange = this.handleToChange.bind(this);
this.state = {
from: undefined,
to: undefined
}
}
componentWillUnmount() {
// same as before
}
focusTo() {
// same as before
}
showFromMonth() {
// same as before
}
handleFromChange(from) {
// same as before
}
handleToChange(to) {
// same as before
}
render() {
const { from, to } = this.state;
const modifiers = { start: from, end: to };
return (
<WrappedComponent
from={from}
to={to}
{...this.props}
>
<DayPickerInput
value={from}
placeholder={trans('common.from')}
format="LL"
dayPickerProps={{
selectedDays: [from, { from, to }],
disabledDays: { after: to },
toMonth: to,
modifiers,
numberOfMonths: 1,
}}
onDayChange={this.handleFromChange}
/>{' '}
—{' '}
<DayPickerInput
ref={el => (this.to = el)}
value={to}
placeholder={trans('common.to')}
format="LL"
dayPickerProps={{
selectedDays: [from, { from, to }],
disabledDays: { before: from },
modifiers,
month: from,
fromMonth: from,
numberOfMonths: 1,
}}
onDayChange={this.handleToChange}
/>
</WrappedComponent>
);
}
};
}
As you can see the code is nearly the same as before, however there are a couple of things that need a little more attention here.
- To original component, called
WrappedComponent
is not modified by our HOC - The
WithDateRange
component passes down two additional props to ourWrappedComponent
, specificallyfrom
andto
that are the dates selected in the pickers. All the unrelated props are passed to theWrappedComponent
using ES6 spread operator. - The
DayPickerInput
components are passed between the opening and closing tag of the original component, so they can be used throughprops.children
by our wrapped component.
We now can apply our HOC to the statistic components that need datepicker functionality:
import AbandonedCarts from './Carts/AbandonedCarts';
import OrdersIncome from './Orders/OrdersIncome';
import OrdersIndex from './Orders/OrdersIndex';
import OrdersStatus from './Orders/OrdersStatus';
import OrdersPayment from './Orders/OrdersPayment';
import MostSelled from './Products/MostSelled';
import CustomersRegistration from './Customers/CustomersRegistration';
import WithDateRange from './WithDateRange';
export default {
'AbandonedCarts': AbandonedCarts,
'OrdersIncome': WithDateRange(OrdersIncome),
'OrdersIndex': WithDateRange(OrdersIndex),
'OrdersStatus': OrdersStatus,
'OrdersPayment': OrdersPayment,
'MostSelled': WithDateRange(MostSelled),
'CustomersRegistration': WithDateRange(CustomersRegistration)
}
Finally, inside one of our enhanced statistic components we use the componentWillReceiveProps
function to react when the component is about to receive new props, checking if from
and to
are defined in order to fetch new data from the backend.
class MostSelled extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
data: null
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.from && nextProps.to) {
const {from, to} = nextProps;
this.fetch({
from: moment(from).format('YYYY-MM-DD'),
to: moment(to).format('YYYY-MM-DD')
});
}
}
render() {
const { loading, data } = this.state;
return (
<div>
{this.props.children}
<hr />
{loading
? <Loading />
: <div>
{this.renderChart(data)}
</div>
}
</div>
);
}
}
Conclusions
HOCs pattern is a powerful tool in the React.js ecosystem and by now it is the standard way for reusing component logic. However we are aware that standards are not cast in stone, but they are destined to change. Mixin themselves are a perfect reminder of this law.
HOCs also present a few problems, summarized by Michael Jackson in a great article in which he favors a different pattern, called Render Props
, over High Order Components. Go check it out!