Editor’s note: Active maintenance of the Recompose library was discontinued as of 25 October 2018. The author recommends using React Hooks instead.
If you like keeping things simple in React by creating small components with functional component syntax and then using them as pieces to create bigger ones, Recompose can help you to do the same with higher-order components (HOCs).
With Recompose, it is easier to create small higher-order components that can be composed into more complex ones. With the approach encouraged by Recompose, you won’t need more Class
syntax to create React components.
But before going into details, let’s start reviewing some concepts…
In JavaScript, we have a special type of function called higher-order functions:
A higher-order function is a function that deals with other functions, either because it receives them as parameters (to execute them at some point of the function’s body), because it returns a new function when it’s called, or both.
const sum = (a, b) => a + b const multiplication = (a, b) => a * b // Our Higher-Order Function const getResultOperation = op => (a, b) => `The ${op.name} of ${a} and ${b} is ${op(a, b)}` const getSumResult = getResultOperation(sum) const getMultiplicationResult = getResultOperation(multiplication) console.log( getSumResult(2, 5) ) // The sum of 2 and 5 is 7 console.log( getMultiplicationResult(2, 5) ) // The multiplication of 2 and 5 is 10
In the example above, getResultOperation
receives a function and returns a new one. So it is a higher-order function.
The most popular higher-order functions in JavaScript are the array methods
map
,filter
orreduce
. They all apply some function passed as a parameter over the elements of the array to get something as a result.
In React, we have the equivalent of higher-order functions, but for components, the so-called higher-order components.
A higher-order component is a function that takes a component and returns a new component.
When are higher-order components useful? Well, mostly to reuse the logic involving behavior across components. Let’s explain this with the following scenario.
Let’s assume we already have a component Button
.
const Button = ({ type = "primary", children, onClick }) => ( <button className={`btn btn-${type}`} onClick={onClick}> {children} </button> );
And we want to create another ButtonWithTrack
based on this Button
(same props on Button
should also work on ButtonWithTrack
and same styles applied) but with improved behavior (like keeping track of the times it has been clicked and displaying this value on the button itself).
To do this, we can do…
import Button from "./Button"; class ButtonWithTrack extends Component { constructor(props) { super(props); this.state = { times: 0 }; } handleClick = e => { let { times } = this.state; const { onClick } = this.props; this.setState({ times: ++times }); onClick && onClick(); }; render() { const { children } = this.props; const { times } = this.state; return ( <span onClick={this.handleClick}> <Button type={times > 5 ? "danger" : "primary"}> {children} <small>{times} times clicked</small> </Button> </span> ); } }
We have reused the original Button
, so everything is OK for now.
Let’s take another component, Link
:
const Link = ({ type = "primary", children, href, onClick }) => ( <a style={styles} className={`badge badge-${type}`} href={href} onClick={onClick}> {children} </a> );
And we want to add the exact same behavior we added to our Button
.
What to do then? Should we repeat 90 percent of the code in two files? Or is there a way we can take out the logic added to ButtonWithTrack
in a way it can be applied to both Button
and Link
components?
Higher-order components to the rescue!!
To solve this problem, we can create a higher-order component, that is, a function that takes one component and returns the enhanced version of that component with the behavior we want.
For example, we can do this:
const withClickTimesTrack = WrappedComponent => class extends Component { constructor(props) { super(props); this.state = { times: 0 }; } handleClick = e => { e.preventDefault(); let { times } = this.state; const { onClick } = this.props; this.setState({ times: ++times }); onClick && onClick(); }; render() { const { children, onClick, ...props } = this.props; const { times } = this.state; return ( <span onClick={this.handleClick}> <WrappedComponent type={times > 5 ? "danger" : "primary"} {...props} > {children} <small>({times} times clicked)</small> </WrappedComponent> </span> ); } };
So then, we can simplify the creation of the component ButtonWithTrack
from Button
by using the withClickTimesTrack
HOC like this:
import withClickTimesTrack from "./hoc/withClickTimesTrack"; const Button = ({ type = "primary", children, onClick }) => ( <button className={`btn btn-${type}`} onClick={onClick}> {children} </button> ); const ButtonWithTrack = withClickTimesTrack(Button);
And also now, we can easily apply the same enhancement to other components like Link
:
import withClickTimesTrack from "./hoc/withClickTimesTrack"; const Link = ({ type = "primary", children, href, onClick }) => ( <a style={styles} className={`badge badge-${type}`} href={href} onClick={onClick}> {children} </a> ); const LinkWithTrack = withClickTimesTrack(Link);
Cool, isn’t it?
But we can think this HOC add too many behaviors at the same time (handler, state & new UI).
Wouldn’t be better if we split the logic behind the HOC into smaller parts?
OK, it’s decided! We want to have these three behaviors of the HOC isolated so we can reuse them independently in other components:
times
statehandleClick
times
state inside the elementTo do this we can create three HOCs where each one will add a specific behavior…
withStateTimes.js
:
const withStateTimes = WrappedComponent => class extends Component { constructor(props) { super(props); this.state = { times: 0 }; } setTimes = (times) => { this.setState({ times }) } render() { const { times } = this.state const { setTimes } = this return ( <WrappedComponent times={times} setTimes={setTimes} { ...this.props } /> ); } };
withHandlerClick.js
:
const withHandlerClick = WrappedComponent => props => { let { times, setTimes, children, onClick, ..._props } = props; const handleClick = e => { e.preventDefault(); setTimes( ++times ); onClick && onClick(); }; return ( {children} ); }
withDisplayTrack.js
:
const withDisplayTrack = WrappedComponent => props => { const { children, onClick, handleClick, times, ..._props } = props; return ( <span onClick={handleClick}> <WrappedComponent type={times > 5 ? "danger" : "primary"} {..._props} > {children} <small>({times} times clicked)</small> </WrappedComponent> </span> ) }
With these three HOCs, we can then apply them to our elements in this way…
const ButtonWithTrack = withStateTimes(withHandlerClick(withDisplayTrack(Button)));
What’s going on here? Well, withDisplayTrack(Button)
returns a component that is used in the call of withHandlerClick
that will also return a component that will be used in the call of withStateTimes
that will return our final component (ButtonWithTrack
).
As you can see, the idea is good because we can reuse our code in this way, but creating these HOCs is a bit complicated and also applying them in this way is a bit hard to read.
Is there any improvement over this?
Recompose to the rescue!! 🙂
What is Recompose? In their own words:
Recompose is a React utility belt for function components and higher-order components. Think of it like lodash for React.
So, it’s a set of methods we can use to improve the organization, creation and application of our HOC’s encouraging the use of functional stateless components combined with the composition of HOCs.
Let’s start with the most-used method of Recompose: compose
.
compose
With compose
, we can compose multiple higher-order components into a single higher-order component.
In our scenario, with compose
, we can now express the application of our HOCs like this:
import { compose } from "recompose"; ... const ButtonWithTrack = compose( withStateTimes, withHandlerClick, withDisplayTrack )(Button)
Much cleaner and easy to read, right?
withState
Another useful method of Recompose for our scenario is withState
.
This method creates a HOC with almost the same behavior we implemented in withStateTimes.js
So, with Recompose, now we can express the same logic like this…
... import { withState } from "recompose"; const withStateTimes = withState('times', 'setTimes', 0) ...
For real? Yes, for real 🙂
The utility of Recompose starts to make sense, right?
withHandlers
Let’s continue improving our scenario’s code. Let’s take the HOC withHandlerClick
. To improve the creation of this HOC, we can use Recompose’s withHandlers
method.
import { withHandlers } from "recompose"; const withHandlerClick = withHandlers({ handleClick: props => e => { let { times, onClick, setTimes } = props; e.preventDefault() setTimes( ++times ); onClick && onClick(); } })
The withHandlers
method takes an object map of handler creators. Each one of the properties of this object passed to withHandlers
should be a higher-order function that accepts a set of props and returns a function handler. In this way we can generate a handler that will have access to the props
of the component.
setDisplayName
In our example, if we debug the code with the React Developer Tools the component returned by withDisplayTrack
is displayed as Unknown
.
To fix this, we can use Recompose’s setDisplayName
to export
a final HOC that will return a component with the name ComponentWithDisplayTrack
.
export default compose( setDisplayName('ComponentWithDisplayTrack'), withDisplayTrack );
lifecycle
With the method lifecycle
we can add lifecycle methods to our functional-syntax components.
In our scenario we could add a different version of Button that display the number of pending messages.
We can create a HOC that returns a different view of our button using a messages
props:
import React from "react"; import { compose, setDisplayName } from "recompose"; const withDisplayMessages = WrappedComponent => props => { const { children, messages, loading, ..._props } = props; return ( <WrappedComponent {..._props}> {children} {loading ? ( <span className="fas fa-spinner fa-pulse"> </span> ) : ( <span className="badge badge-light">{messages}</span> )} </WrappedComponent> ); }; export default compose( setDisplayName("withDisplayMessages"), withDisplayMessages );
And we can add a componentDidMount
lifecycle method to our component that will add:
loading
state set to true
when our fake request starts and set to false
when it finishesmessages
state, which value will be updated with the random number returned by our fake requestBoth loading
and messages
states managed here will add one new prop each to the returned component that will be used to propagate the corresponding values:
import { lifecycle } from "recompose"; const getPendingMessages = () => { const randomNumber = Math.ceil(Math.random() * 10); return new Promise(resolve => { setTimeout(() => resolve(randomNumber), randomNumber * 1000); }); }; const withDidMountStateMessages = lifecycle({ componentDidMount() { this.setState({ loading: true }); getPendingMessages().then(messages => { this.setState({ loading: false, messages }); }); } }); export default withDidMountStateMessages;
With these new HOCs, we can now quickly create our new type of Button
:
const ButtonWithMessages = compose( withDidMountStateMessages, withDisplayMessages )(Button)
defaultProps
With these HOCs, we can transfer these new behaviors into a link with very few lines. And we can add the defaultProps
to change the default type of the link.
const LinkWithMessages = compose( defaultProps({ type: "info" }), withDidMountStateMessages, withDisplayMessages )(Link);
With these methods we can finish our demo by easily creating another version of Button
(just to show the flexibility of this pattern) that track the clicks from three to zero, and adds another prop
so we can change the type
when the countdown reaches zero.
const ButtonWithTrackCountdown = compose( withState('times', 'setTimes', 3), withState('type', 'setType', 'primary'), withHandlers({ handleClick: props => e => { let { times, onClick, setTimes, setType } = props; e.preventDefault() if ( times <= 0 ) { setType('secondary') } else { setTimes( --times ) } onClick && onClick(); } }), withDisplayTrack )(Button)
As you can see, with Recompose it is easier to delegate the logic into small higher-order components and then compose them into a more complex HOC that we can use to create different versions of our components reusing most of our code.
Also, Recompose discourage the use of Class
syntax for creating components and encourage the use of functional stateless components combined with higher components.
The most important advantages of using only function components are:
Basically, once you get how Recompose methods work, it simplifies the development and organization of React components.
There are a lot more of methods that can be used to generate more higher-order components in an easier way.
In the official repo, you can find some Recompose recipes that can be useful to your project.
Also, here you have the code used in this post and a live demo of the result.
So, now that you know a bit more about Recompose… What is your first impression? Do you think is a good way to go when creating components?
My opinion is… that I like it! I really like the patterns encouraged by Recompose oriented to the creation of small and simple pieces (components and HOCs) that can be used to create more complex ones in an easy-to-read way and that are functional programming-oriented.
Well, that’s my opinion. What’s yours?
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMaking carousels can be time-consuming, but it doesn’t have to be. Learn how to use React Snap Carousel to simplify the process.
Consider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
5 Replies to "Using Recompose to write clean higher-order components"
Excellent article JuamMa and good examples. However, I think you may have forgot to change the code for withHandlerClick.js, as it looks the same as withStateTimes.js unless I have misread it…
looks like there is some copy/paste code errors… look at this file you pasted: withHandlerClick.js (it is not the correct one)
Thanks, I just fixed this
Thanks for the article! It’s very useful if you hear about recompose the first time! But you should mention somewhere that the recompose-library recommend to use hooks instead now. I see that this article is from 2018 and hooks were released in 2019 but this information would help a lot ! 🙂
“Hi! I created Recompose about three years ago. About a year after that, I joined the React team. Today, we announced a proposal for Hooks. Hooks solves all the problems I attempted to address with Recompose three years ago, and more on top of that. I will be discontinuing active maintenance of this package (excluding perhaps bugfixes or patches for compatibility with future React releases), and recommending that people use Hooks instead. Your existing code with Recompose will still work, just don’t expect any new features. Thank you so, so much to @wuct and @istarkov for their heroic work maintaining Recompose over the last few years.”
https://github.com/acdlite/recompose
Thanks for pointing this out — as you mentioned, this tutorial is on the older side. We’ve since written a ton about React Hooks:
I’ve added an editor’s note to this tutorial for context. Thanks again!