-->

Using Recompose to build higher-order components

2017-09-12

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 applilcation and we need to build payment page. Here are the modes of the 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 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 pricipal 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} />
  );
}

«««< HEAD First let’s handle the case where the payment mode happens to be CashOnDelivery. First let’s see the changed code. ======= First let’s handle the case of the payment mode is CashOnDelivery.

tmp

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.

Branch takes three arguments and returns a HOC. The first argument is a predicate which takes props as argument and «««< HEAD must return a Boolean value. ======= returns a Boolean value.

tmp The second and third arguments are higher-order components. If the predicate evaluates to true then left HOC is rendered otherwise 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 of the branch is optional, we do not need to supply it. If we don’t supply third arugment 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 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 its 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 follow similar pattern.

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 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 that condition is mentioned 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 size and do only one thing.

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

2017-09-12

Yazıda Geçen Linkler
  1. github.com
  2. github.io
  3. wikipedia.org
  4. ramdajs.com
  5. squareup.com
  6. bitcoin.org

http://blog.bigbinary.com/2017/09/12/using-recompose-to-build-higher-order-components.html