In this post, we'll discuss using the recompose
library and higher-order components in React. We'll touch on the following:
- What are higher-order components and why are they useful?
- What is
recompose
and how do we use it to generate higher-order components? - Resources
Higher-order components (HOCs) are components that take a component as an argument and return a new component with additional functionality. If you're familiar with the decorator pattern, this should ring a bell for you.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
These can be useful in a number of situations. Below is a short list of examples, which we'll go through with examples momentarily:
- DRYing up components. Instead of duplicating shared functionality in two components, encapsulate it in an HOC that can wrap each component.
- Adding state to presentational components
- Determining which component to render given a condition (e.g. if user is authenticated)
recompose
is a library that provides two primary categories of functions:
- HOCs built to solve common problems, such as
withState
to bestow state on presentational components orlifecycle
to add lifecycle hooks. - Utility functions, such as
compose
, a function that streamlines the syntax for using multiple HOCs, orenhance
.
You can view a full list of the HOCs recompose
provides here: https://github.com/acdlite/recompose/blob/master/docs/API.md
We'll touch on a few of them here.
mapProps
provides a function that intercepts the props passed into the wrapper component and transforms them into a new set of props to pass to the wrapped component.
Here's a simple example where we use mapProps
to create a higher order component that will uppercase all prop values before passing them on to its wrapped component:
// src/components/MyWrapperComponent/enhance.js
import { mapProps } from 'recompose'
// the function to be used to transform the props passed into the wrapper component
const propsMapper = props => {
let transformedProps = {}
Object.keys(props).map(prop => {
let key = prop[0]
let value = prop[1]
transformedProps[key] = value.toUpperCase()
})
return transformedProps
}
// high order component function. When called with props, `enhance` will generate a component wrapping the component passed in.
export default enhance = mapProps(propsMapper)
// src/components/MyWrapperComponent/index.js
import enhance from './enhance'
const MyWrappedComponent = ({
name,
address,
email
}) = (
<div className="container">
<div className="name">{name}</div>
<div className="address">{address}</div>
<div className="email">{email}</div>
</div>
)
export default enhance(MyWrappedComponent)
import MyWrapperComponent from '/path/to/MyWrapperComponent'
// `MyWrappedComponent` will receive ``{ foo: "APPLE", bar: "BANANA" }`
<MyWrapperComponent foo="apple" bar="banana" />
withProps
does the same but also passes the untransformed props down to the wrapped component.
defaultProps
, as you'd expect, "specifies propse to be passed by default to the base component".
The withHandlers
HOC allows you to pass in different event handlers (e.g. handlers for onChange
events), to a wrapped component. (Note that you can pass handlers likes this in via mapProps
as well, but there are some performance benefits to using withHandlers
instead.)
The withState
HOC allows you to pass state, along with a function to update that state, to a wrapped component. You can use this in combination with withHandlers
to pass in multiple functions to change the state, or you can use withStateHandlers
to do both in a single HOC. See the docs for example code.
branch
is useful for rendering different components based on whether a condition is met. We might use this if we want to render a login screen for unauthenticated users, a loading spinner until data has been loaded, or an error component if we encounter a server error.
The lifecycle
HOC allows you to pass in functions to be called at different lifecycle points, for example componentDidMount
. See the docs for an example.
These HOCs allow you to rename props from the key used when they are passed in, to a key expected by the wrapped component.
These are just a few of the HOCs provided by recompose
. Refer to the full list for additional information.
In addition to providing these HOCs, recompose
provides some utility functions to make them easier to work with. The most commonly used of these is compose
.
compose
allows you combine multiple HOCs into a single HOC. This is useful if, for example, you want to both use mapProps
to transform props passed to the base and also use withState
to set state, branch
to render different components, lifecycle
to pass in lifecycle hooks, etc.
Let's update our example with mapProps
to also pass in an isCollapsed
state to our component:
// src/components/MyWrapperComponent/enhance.js
import { mapProps, compose } from 'recompose'
const propsMapper = props => {
let transformedProps = {}
Object.keys(props).map(prop => {
let key = prop[0]
let value = prop[1]
transformedProps[key] = value.toUpperCase()
})
return transformedProps
}
export default enhance = compose(
mapProps(propsMapper),
withState('isCollapsed', 'setIsCollapsed', true)
)
Below are some resources (curated by @davidrf :prayer_hands:) for learning more about HOCs and recompose
. For examples of how we've used this in our react apps at Intrepid, ask @davidrf or others. Enjoy!
- Andrew Clark - React Europe 2016: https://www.youtube.com/watch?v=zD_judE-bXk
- Nik Graf - React Europe 2017: https://www.youtube.com/watch?v=qJgff2spvzM
- Higher Order Components with Functional Patterns Using Recompose (if you have an egghead.io account): https://egghead.io/courses/higher-order-components-with-functional-patterns-using-recompose
- Recompose API: https://github.com/acdlite/recompose/blob/master/docs/API.md