le0.io

Recompose & React Hooks: An API-focused comparison of enhancer libraries

6th March, 2019 - 6 min read

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).

What's an "enhancer"?
Both Recompose and React Hooks supply 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.

TLDR
In this post I'll outline the advantages provided by enhancer patterns (used with both Recompose & Hooks), illustrated with equivalent Hooks for some popular Recompose enhancers.

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:

Don't worry if you're unfamiliar with the Recompose API - I'll go over these specific enhancers in more detail in a later section.

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.

Check out the catchy component names too!

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.

React docs strongly recommend starting Hook names with 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.

What? No Hook?
Structuring all our enhancers in a predictable way will help reduce mental overhead, as you don't have to keep track of what is a Hook and what isn't. It doesn't really matter, as long as they 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 memoizing 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.

The function we pass to 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.

While in this example I'm just trying to recreate older functionality, in practice the 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.