Using Recompose to build higher-order components

Arbaaz

Arbaaz

September 12, 2017

Recompose is a toolkit for writing React components using higher-order components. Recompose allows us to write many smaller higher-order components and then we compose all those components together to get the desired component. It improves both readability and the maintainability of the code.

HigherOrderComponents are also written as HOC. Going forward we will use HOC to refer to higher-order components.

Using Recompose in an e-commerce application

We are working on an e-commerce application and we need to build payment page. Here are the modes of payment.

  • Online
  • Cash on delivery
  • Swipe on delivery

We need to render our React components depending upon the payment mode selected by the user. Typically we render components based on some state.

Here is the traditional way of writing code.

1state = {
2  showPayOnlineScreen: true,
3  showCashOnDeliveryScreen: false,
4  showSwipeOnDeliveryScreen: false,
5}
6
7renderMainScreen = () => {
8  const { showCashOnDeliveryScreen, showSwipeOnDeliveryScreen } = this.state;
9
10  if (showCashOnDeliveryScreen) {
11    return <CashOnDeliveryScreen />;
12  } else if (showSwipeOnDeliveryScreen) {
13    return <SwipeOnDeliveryScreen />;
14  }
15  return <PayOnlineScreen />;
16}
17
18 render() {
19  return (
20    { this.renderMainScreen() }
21  );
22 }
23

We will try to refactor the code using the tools provided by Recompose.

In general, the guiding principle of functional programming is composition. So here we will assume that the default payment mechanism is online. If the payment mode happens to be something else then we will take care of it by enhancing the existing component.

So to start with our code would look like this.

1state = {
2  paymentType: online,
3}
4
5render() {
6  return (
7    <PayOnline {...this.state} />
8  );
9}

First let's handle the case of payment mode CashOnDelivery.

1import { branch, renderComponent, renderNothing } from 'recompose';
2import CashScreen from 'components/payments/cashScreen';
3
4const cashOnDelivery = 'CASH_ON_DELIVERY';
5
6const enhance = branch(
7  (props) => (props.paymentType === cashOnDelivery)
8  renderComponent(CashScreen),
9  renderNothing
10)

Recompose has branch function which acts like a ternary operator.

The branch function accepts three arguments and returns a HOC. The first argument is a predicate which accepts props as the argument and returns a Boolean value. The second and third arguments are higher-order components. If the predicate evaluates to true then the left HOC is rendered otherwise the right HOC is rendered. Here is how branch is implemented.

1branch(
2  test: (props: Object) => boolean,
3  left: HigherOrderComponent,
4  right: ?HigherOrderComponent
5): HigherOrderComponent

Notice the question mark in ?HigherOrderComponent. It means that the third argument is optional.

If you are familiar with Ramdajs then this is similar to ifElse in Ramdajs.

renderComponent takes a component and returns an HOC version of it.

renderNothing is an HOC which will always render null.

Since the third argument to branch is optional, we do not need to supply it. If we don't supply the third argument then that means the original component will be rendered.

So now we can make our code shorter by removing usage of renderNothing.

1const enhance = branch(
2  (props) => (props.paymentType === cashOnDelivery)
3  renderComponent(CashScreen)
4)
5
6const MainScreen = enhance(PayOnlineScreen);

Next condition is handling SwipeOnDelivery

SwipeOnDelivery means that upon delivery customer pays using credit card using Square or a similar tool.

We will follow the same pattern and the code might look like this.

1import { branch, renderComponent } from 'recompose';
2import CashScreen from 'components/payments/CashScreen';
3import PayOnlineScreen from 'components/payments/PayOnlineScreen';
4import CardScreen from 'components/payments/CardScreen';
5
6const cashOnDelivery = 'CASH_ON_DELIVERY';
7const swipeOnDelivery = 'SWIPE_ON_DELIVERY';
8
9let enhance = branch(
10  (props) => (props.paymentType === cashOnDelivery)
11  renderComponent(CashScreen),
12)
13
14enhance = branch(
15  (props) => (props.paymentType === swipeOnDelivery)
16  renderComponent(CardScreen),
17)(enhance)
18
19const MainScreen = enhance(PayOnlineScreen);

Extracting out predicates

Let's extract predicates into their own functions.

1import { branch, renderComponent } from "recompose";
2import CashScreen from "components/payments/CashScreen";
3import PayOnlineScreen from "components/payments/PayOnlineScreen";
4import CardScreen from "components/payments/CardScreen";
5
6const cashOnDelivery = "CASH_ON_DELIVERY";
7const swipeOnDelivery = "SWIPE_ON_DELIVERY";
8
9// predicates
10const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;
11
12const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;
13
14let enhance = branch(isCashOnDelivery, renderComponent(CashScreen));
15
16enhance = branch(isSwipeOnDelivery, renderComponent(CardScreen))(enhance);
17
18const MainScreen = enhance(PayOnlineScreen);

Adding one more payment method

Let's say that next we need to add support for Bitcoin.

We can use the same process.

1const cashOnDelivery = "CASH_ON_DELIVERY";
2const swipeOnDelivery = "SWIPE_ON_DELIVERY";
3const bitcoinOnDelivery = "BITCOIN_ON_DELIVERY";
4
5const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;
6
7const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;
8
9const isBitcoinOnDelivery = ({ paymentType }) =>
10  paymentType === bitcoinOnDelivery;
11
12let enhance = branch(isCashOnDelivery, renderComponent(CashScreen));
13
14enhance = branch(isSwipeOnDelivery, renderComponent(CardScreen))(enhance);
15
16enhance = branch(isBitcoinOnDelivery, renderComponent(BitcoinScreen))(enhance);
17
18const MainScreen = enhance(PayOnlineScreen);

You can see the pattern and it is getting repetitive and boring. We can chain these conditions together to make it less repetitive.

Let's use the compose function and chain them.

1const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;
2
3const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;
4
5const cashOnDeliveryCondition = branch(
6  isCashOnDelivery,
7  renderComponent(CashScreen)
8);
9
10const swipeOnDeliveryCondition = branch(
11  isSwipeOnDelivery,
12  renderComponent(CardScreen)
13);
14
15const enhance = compose(cashOnDeliveryCondition, swipeOnDeliveryCondition);
16
17const MainScreen = enhance(PayOnlineScreen);

Refactoring code to remove repetition

At this time we are building a condition (like cashOnDeliveryCondition) for each payment type and then using that condition in compose. We can put all such conditions in an array and then we can use that array in compose. Let's see it in action.

1const cashOnDelivery = "CASH_ON_DELIVERY";
2const swipeOnDelivery = "SWIPE_ON_DELIVERY";
3
4const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;
5
6const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;
7
8const states = [
9  {
10    when: isCashOnDelivery,
11    then: CashOnDeliveryScreen,
12  },
13  {
14    when: isSwipeOnDelivery,
15    then: SwipeOnDeliveryScreen,
16  },
17];
18
19const componentsArray = states.map(({ when, then }) =>
20  branch(when, renderComponent(then))
21);
22
23const enhance = compose(...componentsArray);
24
25const MainScreen = enhance(PayOnlineScreen);

Extract function for reusability

We are going to extract some code in utils for better reusability.

1// utils/composeStates.js
2
3import { branch, renderComponent, compose } from "recompose";
4
5export default function composeStates(states) {
6  const componentsArray = states.map(({ when, then }) =>
7    branch(when, renderComponent(then))
8  );
9
10  return compose(...componentsArray);
11}

Now our main code looks like this.

1import composeStates from "utils/composeStates.js";
2
3const cashOnDelivery = "CASH_ON_DELIVERY";
4const swipeOnDelivery = "SWIPE_ON_DELIVERY";
5
6const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;
7
8const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;
9
10const states = [
11  {
12    when: isCashOnDelivery,
13    then: CashScreen,
14  },
15  {
16    when: isSwipeOnDelivery,
17    then: CardScreen,
18  },
19];
20
21const enhance = composeStates(states);
22
23const MainScreen = enhance(PayOnlineScreen);

Full before and after comparison

Here is before code.

1import React, { Component } from "react";
2import PropTypes from "prop-types";
3import { connect } from "react-redux";
4import { browserHistory } from "react-router";
5import { Modal } from "react-bootstrap";
6import * as authActions from "redux/modules/auth";
7import PaymentsModalBase from "../../components/PaymentsModal/PaymentsModalBase";
8import PayOnlineScreen from "../../components/PaymentsModal/PayOnlineScreen";
9import CashScreen from "../../components/PaymentsModal/CashScreen";
10import CardScreen from "../../components/PaymentsModal/CardScreen";
11
12@connect(() => ({}), { ...authActions })
13export default class PaymentsModal extends Component {
14  static propTypes = {
15    show: PropTypes.bool.isRequired,
16    hideModal: PropTypes.func.isRequired,
17    orderDetails: PropTypes.object.isRequired,
18  };
19
20  static defaultProps = {
21    show: true,
22    hideModal: () => {
23      browserHistory.push("/");
24    },
25    orderDetails: {},
26  };
27
28  state = {
29    showOnlineScreen: true,
30    showCashScreen: false,
31    showCardScreen: false,
32  };
33
34  renderScreens = () => {
35    const { showCashScreen, showCardScreen } = this.state;
36
37    if (showCashScreen) {
38      return <CashScreen />;
39    } else if (showCardScreen) {
40      return <CardScreen />;
41    }
42    return <PayOnlineScreen />;
43  };
44
45  render() {
46    const { show, hideModal, orderDetails } = this.props;
47    return (
48      <Modal show={show} onHide={hideModal} dialogClassName="modal-payments">
49        <PaymentsModalBase orderDetails={orderDetails} onHide={hideModal}>
50          {this.renderScreens()}
51        </PaymentsModalBase>
52      </Modal>
53    );
54  }
55}

Here is after applying recompose code.

1import React, { Component } from "react";
2import PropTypes from "prop-types";
3import { connect } from "react-redux";
4import { Modal } from "react-bootstrap";
5import { compose, branch, renderComponent } from "recompose";
6import * as authActions from "redux/modules/auth";
7import PaymentsModalBase from "components/PaymentsModal/PaymentsModalBase";
8import PayOnlineScreen from "components/PaymentsModal/PayOnlineScreen";
9import CashOnDeliveryScreen from "components/PaymentsModal/CashScreen";
10import SwipeOnDeliveryScreen from "components/PaymentsModal/CardScreen";
11
12const cashOnDelivery = "CASH_ON_DELIVERY";
13const swipeOnDelivery = "SWIPE_ON_DELIVERY";
14const online = "ONLINE";
15
16const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;
17const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;
18
19const conditionalRender = (states) =>
20  compose(
21    ...states.map((state) => branch(state.when, renderComponent(state.then)))
22  );
23
24const enhance = compose(
25  conditionalRender([
26    { when: isCashOnDelivery, then: CashOnDeliveryScreen },
27    { when: isSwipeOnDelivery, then: SwipeOnDeliveryScreen },
28  ])
29);
30
31const PayOnline = enhance(PayOnlineScreen);
32
33@connect(() => ({}), { ...authActions })
34export default class PaymentsModal extends Component {
35  static propTypes = {
36    isModalVisible: PropTypes.bool.isRequired,
37    hidePaymentModal: PropTypes.func.isRequired,
38    orderDetails: PropTypes.object.isRequired,
39  };
40
41  state = {
42    paymentType: online,
43  };
44
45  render() {
46    const { isModalVisible, hidePaymentModal, orderDetails } = this.props;
47    return (
48      <Modal
49        show={isModalVisible}
50        onHide={hidePaymentModal}
51        dialogClassName="modal-payments"
52      >
53        <PaymentsModalBase
54          orderDetails={orderDetails}
55          hidePaymentModal={hidePaymentModal}
56        >
57          <PayOnline {...this.state} />
58        </PaymentsModalBase>
59      </Modal>
60    );
61  }
62}

Functional code is a win

Functional code is all about composing smaller functions together like lego pieces. It results in better code because functions are usually smaller in size and do only one thing.

In coming weeks we will see more applications of recompose in the real world.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.