Practical use case for React.js High Order components

Reading time ~6 minutes

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.

A peak of the final result

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 our WrappedComponent, specifically from and to that are the dates selected in the pickers. All the unrelated props are passed to the WrappedComponent using ES6 spread operator.
  • The DayPickerInput components are passed between the opening and closing tag of the original component, so they can be used through props.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!

comments powered by Disqus

Split char-separated values in MySQL

Introduction

Recently I needed to extract a list of URLs from a MySQL table. All those URLs were inserted in a single TEXT column, …