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.

state = {
  showPayOnlineScreen: true,
  showCashOnDeliveryScreen: false,
  showSwipeOnDeliveryScreen: false,
}

renderMainScreen = () => {
  const { showCashOnDeliveryScreen, showSwipeOnDeliveryScreen } = this.state;

  if (showCashOnDeliveryScreen) {
    return <CashOnDeliveryScreen />;
  } else if (showSwipeOnDeliveryScreen) {
    return <SwipeOnDeliveryScreen />;
  }
  return <PayOnlineScreen />;
}

 render() {
  return (
    { this.renderMainScreen() }
  );
 }

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.

state = {
  paymentType: online,
}

render() {
  return (
    <PayOnline {...this.state} />
  );
}

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

import { branch, renderComponent, renderNothing } from 'recompose';
import CashScreen from 'components/payments/cashScreen';

const cashOnDelivery = 'CASH_ON_DELIVERY';

const enhance = branch(
  (props) => (props.paymentType === cashOnDelivery)
  renderComponent(CashScreen),
  renderNothing
)

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.

branch(
  test: (props: Object) => boolean,
  left: HigherOrderComponent,
  right: ?HigherOrderComponent
): 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.

const enhance = branch(
  (props) => (props.paymentType === cashOnDelivery)
  renderComponent(CashScreen)
)

const 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.

import { branch, renderComponent } from 'recompose';
import CashScreen from 'components/payments/CashScreen';
import PayOnlineScreen from 'components/payments/PayOnlineScreen';
import CardScreen from 'components/payments/CardScreen';

const cashOnDelivery = 'CASH_ON_DELIVERY';
const swipeOnDelivery = 'SWIPE_ON_DELIVERY';

let enhance = branch(
  (props) => (props.paymentType === cashOnDelivery)
  renderComponent(CashScreen),
)

enhance = branch(
  (props) => (props.paymentType === swipeOnDelivery)
  renderComponent(CardScreen),
)(enhance)

const MainScreen = enhance(PayOnlineScreen);

Extracting out predicates

Let's extract predicates into their own functions.

import { branch, renderComponent } from "recompose";
import CashScreen from "components/payments/CashScreen";
import PayOnlineScreen from "components/payments/PayOnlineScreen";
import CardScreen from "components/payments/CardScreen";

const cashOnDelivery = "CASH_ON_DELIVERY";
const swipeOnDelivery = "SWIPE_ON_DELIVERY";

// predicates
const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;

const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;

let enhance = branch(isCashOnDelivery, renderComponent(CashScreen));

enhance = branch(isSwipeOnDelivery, renderComponent(CardScreen))(enhance);

const 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.

const cashOnDelivery = "CASH_ON_DELIVERY";
const swipeOnDelivery = "SWIPE_ON_DELIVERY";
const bitcoinOnDelivery = "BITCOIN_ON_DELIVERY";

const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;

const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;

const isBitcoinOnDelivery = ({ paymentType }) =>
  paymentType === bitcoinOnDelivery;

let enhance = branch(isCashOnDelivery, renderComponent(CashScreen));

enhance = branch(isSwipeOnDelivery, renderComponent(CardScreen))(enhance);

enhance = branch(isBitcoinOnDelivery, renderComponent(BitcoinScreen))(enhance);

const 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.

const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;

const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;

const cashOnDeliveryCondition = branch(
  isCashOnDelivery,
  renderComponent(CashScreen)
);

const swipeOnDeliveryCondition = branch(
  isSwipeOnDelivery,
  renderComponent(CardScreen)
);

const enhance = compose(cashOnDeliveryCondition, swipeOnDeliveryCondition);

const 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.

const cashOnDelivery = "CASH_ON_DELIVERY";
const swipeOnDelivery = "SWIPE_ON_DELIVERY";

const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;

const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;

const states = [
  {
    when: isCashOnDelivery,
    then: CashOnDeliveryScreen,
  },
  {
    when: isSwipeOnDelivery,
    then: SwipeOnDeliveryScreen,
  },
];

const componentsArray = states.map(({ when, then }) =>
  branch(when, renderComponent(then))
);

const enhance = compose(...componentsArray);

const MainScreen = enhance(PayOnlineScreen);

Extract function for reusability

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

// utils/composeStates.js

import { branch, renderComponent, compose } from "recompose";

export default function composeStates(states) {
  const componentsArray = states.map(({ when, then }) =>
    branch(when, renderComponent(then))
  );

  return compose(...componentsArray);
}

Now our main code looks like this.

import composeStates from "utils/composeStates.js";

const cashOnDelivery = "CASH_ON_DELIVERY";
const swipeOnDelivery = "SWIPE_ON_DELIVERY";

const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;

const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;

const states = [
  {
    when: isCashOnDelivery,
    then: CashScreen,
  },
  {
    when: isSwipeOnDelivery,
    then: CardScreen,
  },
];

const enhance = composeStates(states);

const MainScreen = enhance(PayOnlineScreen);

Full before and after comparison

Here is before code.

import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { browserHistory } from "react-router";
import { Modal } from "react-bootstrap";
import * as authActions from "redux/modules/auth";
import PaymentsModalBase from "../../components/PaymentsModal/PaymentsModalBase";
import PayOnlineScreen from "../../components/PaymentsModal/PayOnlineScreen";
import CashScreen from "../../components/PaymentsModal/CashScreen";
import CardScreen from "../../components/PaymentsModal/CardScreen";

@connect(() => ({}), { ...authActions })
export default class PaymentsModal extends Component {
  static propTypes = {
    show: PropTypes.bool.isRequired,
    hideModal: PropTypes.func.isRequired,
    orderDetails: PropTypes.object.isRequired,
  };

  static defaultProps = {
    show: true,
    hideModal: () => {
      browserHistory.push("/");
    },
    orderDetails: {},
  };

  state = {
    showOnlineScreen: true,
    showCashScreen: false,
    showCardScreen: false,
  };

  renderScreens = () => {
    const { showCashScreen, showCardScreen } = this.state;

    if (showCashScreen) {
      return <CashScreen />;
    } else if (showCardScreen) {
      return <CardScreen />;
    }
    return <PayOnlineScreen />;
  };

  render() {
    const { show, hideModal, orderDetails } = this.props;
    return (
      <Modal show={show} onHide={hideModal} dialogClassName="modal-payments">
        <PaymentsModalBase orderDetails={orderDetails} onHide={hideModal}>
          {this.renderScreens()}
        </PaymentsModalBase>
      </Modal>
    );
  }
}

Here is after applying recompose code.

import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Modal } from "react-bootstrap";
import { compose, branch, renderComponent } from "recompose";
import * as authActions from "redux/modules/auth";
import PaymentsModalBase from "components/PaymentsModal/PaymentsModalBase";
import PayOnlineScreen from "components/PaymentsModal/PayOnlineScreen";
import CashOnDeliveryScreen from "components/PaymentsModal/CashScreen";
import SwipeOnDeliveryScreen from "components/PaymentsModal/CardScreen";

const cashOnDelivery = "CASH_ON_DELIVERY";
const swipeOnDelivery = "SWIPE_ON_DELIVERY";
const online = "ONLINE";

const isCashOnDelivery = ({ paymentType }) => paymentType === cashOnDelivery;
const isSwipeOnDelivery = ({ paymentType }) => paymentType === swipeOnDelivery;

const conditionalRender = (states) =>
  compose(
    ...states.map((state) => branch(state.when, renderComponent(state.then)))
  );

const enhance = compose(
  conditionalRender([
    { when: isCashOnDelivery, then: CashOnDeliveryScreen },
    { when: isSwipeOnDelivery, then: SwipeOnDeliveryScreen },
  ])
);

const PayOnline = enhance(PayOnlineScreen);

@connect(() => ({}), { ...authActions })
export default class PaymentsModal extends Component {
  static propTypes = {
    isModalVisible: PropTypes.bool.isRequired,
    hidePaymentModal: PropTypes.func.isRequired,
    orderDetails: PropTypes.object.isRequired,
  };

  state = {
    paymentType: online,
  };

  render() {
    const { isModalVisible, hidePaymentModal, orderDetails } = this.props;
    return (
      <Modal
        show={isModalVisible}
        onHide={hidePaymentModal}
        dialogClassName="modal-payments"
      >
        <PaymentsModalBase
          orderDetails={orderDetails}
          hidePaymentModal={hidePaymentModal}
        >
          <PayOnline {...this.state} />
        </PaymentsModalBase>
      </Modal>
    );
  }
}

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.