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.
We are working on an e-commerce application and we need to build payment page. Here are the modes of payment.
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);
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);
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);
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);
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);
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);
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 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.