Recompose & React Hooks: An API-focused comparison of enhancer libraries
Recompose is awesome. I've been using it on some larger React projects to supercharge functional components with shareable enhancers, some of which use features like state (yes, inside a functional component).
enhancers
, aka some functionality to enhance
a component with new features, usually in the form of additional props and handlers. There's some examples in the next section.For reasons then unknown, Recompose development has stagnated for a while. This was disappointing, as the APIs the library provided to create shareable, composable enhancers felt essential in a way no other 3rd party APIs had before.
Shortly after the announcement of React Hooks, Recompose announced that it was ceasing active new feature development. Things began to make sense. This note from Andrew Clark sums the situation up nicely:
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 (...), and recommending that people use Hooks instead.
I've been using Hooks to build this blog, and initial impressions are that they do indeed feel like a natural evolution of Recompose - this time, provided by the core React library. However, Recompose and Hooks feature substantially different APIs, so it's not immediatley obvious how to do migrate cleanly.
Recompose & The Enhancer Pattern
Recompose is an enhancer library, providing convenient Higher Order Components (HOCs) for common React patterns. This allows us to incrementally add features and functionality to a component (aka "enhance" it) via composition.
The enhancer pattern has some significant benefits:
- Enhancers can be easily shared between components with similar concerns
- Boilerplate code is significantly reduced for common functionality (eg adding
state
) - Logical complexity can be broken up into smaller, more digestable chunks
- Numerous smaller enhancers can be stacked via their inputs and outputs, creating a functional-like coding style
The Recompose pattern I'd settled on just before Hooks entered the picture looked something like this:
Step 1 - The UI Component
Write a logicless functional (or UI) component. This component should avoid logic statements, instead implicitly returning JSX with the assumption that the it will magically receive all the props it needs. This is similar to the presentational component pattern of yore.
As a guideline, if your UI component has an explicit return
statement, you're probably not writing a strict UI component.
Step 2 - The Enhancers
Incrementally add the props you need via enhancers. In this case, I'd like to calculate a greeting from a name, so I'd create an enhancer like this:
My UI component requires some basic state, so let's add that too.
Step 3 - Composition
Finally, tie everything together with the included compose
helper.
As you can see below, I've split what would have been one larger component into two smaller enhancers and one UI component. I can then go on to test these smaller enhancers individually, and even share some of these enhancers with other UI components if I want to.
Issues with Recompose & HOCs
While Recompose is extremely useful, it does have some notable downsides.
As it is based on the HOC pattern, each enhancer is also a standalone React component. This is not an entirely ergonomic fit for simply enhancing a component, for a few reasons.
Enhancer HOCs usually never render any JSX of their own, instead simply adding or altering the props passed down to the next component. These HOCs are simply vehicles for business logic, so their React component shell isn't really necessary.
Some HOCs supplied by Recompose suffer from an inverse issue. Enhancers such as withState
do not store any business logic, instead simply delivering a slice of class-based functionality down to a functional component (in this case, state
functionality). Enhancers like this serve only as a workaround for the feature limitations of functional components.
As each enhancer is a complete component, React Devtools can easily become a complete mess. A single inner component wrapped in numerous HOCs would create an unwieldy component tree - displayed as something resembling callback hell. This makes debugging in Devtools painful and difficult.
Internally, React does not delineate between components that render their own JSX vs components that simply pass something down to an inner component. What we need is a new way to enhance our components without creating additional intermediate components in the process.
Hooks to the rescue
Hooks manage to address HOC enhancer issues elegantly, in part by being humble functions instead of full-fat React components.
They don't contain any render
functionality, and they don't need to. Instead, they simply supply React components with something they need and handle re-rendering the parent component when required. As they are simply functions, the underlying component tree is significantly decluttered.
API Replacements
Not all the solutions below strictly involve Hooks, but they all aim to use the enhancer pattern in a consistent way.
use
, eg useGreeting
. It's too soon to tell if it makes sense to stretch this nomenclature to include all enhancers (hook-based or not), but for consistency it I'm gonna do that here.withState
As one of the most convenient enhancers in Recompose's arsenal, this one has earned itself a directly equivalent Hook. Both take an optional initial state and return the current state, along with a handler to replace this entire current state.
withStateHanders
While withState
is just simple getter/setter (completely replacing the entire state each time), withStateHanders
allows us to capture more complex logical interactions with this state.
Instead of providing one update function, this enhancer allows us to create many custom handlers which can update the contained state in a more targeted way. These handlers can access both current props and internal state, and in return update a slice of the internal state instead of replacing it. In this way, it functions much more like calling setState
inside a class method handler.
React provides a unique equivalent to this functionality in the form of custom Hooks. Pleasingly, custom Hooks can do a lot more than just interact with a state object - they may combine any number of Hooks with vanilla JavaScript to form a complete, encapsulated solution for code sharing.
Custom hooks are much more general purpose than withStateHandlers
, but for comparison here's a direct equivalent.
withProps
Recompose's withProps
accepts input props, performs some calculations and then returns a set of output props to be merged into the next component's props.
Since Hooks don't explicitly plug into component props like this (as they are just functions), we can instead use a vanilla JavaScript function to encapsulate the logic found within withProps
.
It's up to you to decide if you prefer creating a function which groups similar value calculations into a single return, or if you want to separate each value into it's own function.
enhance
🔬.withPropsOnChange
Most child enhancers will re-run on every parent re-render. For pure functions, this can be inefficient - for any given input, the output props will always be the same.
We can avoid re-running these calculations by memo
izing them. withPropsOnChange
accepts a new initial argument which can be one of the following:
- An array of prop names, as strings, to perform an equality check on between renders
- A function that accepts the previous and next props, returning a boolean (similar to
shouldComponentUpdate
)
The evident replacement here would be the useMemo
Hook. It takes two arguments, the first being a function and the second being an array of variables (the variables themselves, not the names as strings a-la Recompose).
If any of the variables in the array change between renders, the passed function is re-run and the result is returned from useMemo
. If not, the previously memoized value is returned.
useMemo
does not contain include any arguments - the variables used in the calculation are instead inherited from the parent scope.React now also provides a React.memo
utility to wrap pure functional components and only re-render them if any of the props change - a functional equivalent to React.PureComponent
. However, this encapsulates the entire component and not the enhancers within, so it's not as granular as we would like for this example.
withHandlers
Handlers have historically been a bit tricky to deal with in React, since they often need to access both the arguments passed to them and the current props of the component. We can bind
the props we need to the handler, or create an anonymous inline function which calls the handler with the props from the parent scope.
In both instances, a new function is created on every re-render.
This is inefficient and can also cause unnecessary re-renders further down the chain, as child components have no way of knowing if the new handler passed to it contains the same props it did before. As they are different functions in memory, the equality check will always fail.
withHandlers
allows us to pass handlers which have access to both the current props and any passed arguments, without having to worry about optimization. It memoizes the function and only returns a new function when the props change.
Finally, there's an official React solution. The useCallback
Hook returns a memoized callback which only returns a new function when the provided variables also change. This ensures that function references remain consistent between renders, only changing when they need to.
lifecycle
Functional components can gain access to the Component API via this enhancer.
While the full Component API still resides exclusively in React.Component
, Hooks do provide us with equivalents for some popular lifecycle use cases, namely componentDidMount
, componentDidUpdate
and componentWillUnmount
all in one handy Hook: useEffect
.
This Hook provides an official way to trigger "side effects" and is usually run on every render, however here we'll focus on equivalent functionality for the lifecycles mentioned above.
The most important thing to note here is the second optional argument of useEffect
- an array of variables used to determine when the Hook is run. for our use case, this should either be an empty array or nothing at all.
useEffect
Hook is not a direct equivalent for these methods but instead a new paradigm for dealing with component side effects. It's best to see it a brand new tool for solving specific issues previously addressed by certan lifecycle methods.branch
This enhancer can render one of two components depending on the props passed from the chain.
The Recompose version of this enhancer is worth noting as it has no good equivalent Hook. HOCs work at component level and thus have control over the component provided to the enhancer, even switching it for a different component if required. Hooks only exist inside an already invoked component.
Due to this change in functionality, branch
is best replaced by a custom component rather than a Hook.
Conclusion
While this is by no means a exhaustive list of all Recompose / Hook enhancers, the examples above should illustrate their equivalency.
The enhancer pattern is incredibly useful and deserves to be part of the official top-level API, as it now is with Hooks. Even though they're still fresh, I'm sold on Hooks as a Recompose replacement thanks to their simplicity, flexibility and ease of use.