Higher Order Components in a React Hooks World

Photo by Franck V. on Unsplash

For most people working with regularly with React, it’s hard to go very far without encountering higher-order components (or HoC’s). To the uninitiated, it is a pattern that leverages React’s compositional nature to allow for code reuse across different components. Even if you haven’t written any yourself, chances are likely that you too are using them in some capacity without even realizing it.

Common usage patterns can range wildly from connecting to a project scoped state management store (ex: connect for redux), to conditional rendering components or my favorite use case of giving functional components local state.

Conceptually HoC’s operate by wrapping a base component with some function called an enhancer. That enhancer can transform or affect a base component in any number of ways. It is similar to the map function in how it can apply to collections of data in order to transform data into something new. If that’s still a little confusing, consider this example:

// A simple component
const HelloComponent = ({ name, ...otherProps }) =>
(<div {...otherProps} >
Hello {name}!
</div>);
// An enhancer that will set a name prop on 
// a base component to "New Name"
const withNameOverride = BaseComponent => props =>
(<BaseComponent
{...props}
name="New Name"
/>);
// An enhancer that will apply some inline
// styling to a base component
const withStyling = BaseComponent => props =>
(<BaseComponent
{...props}
style={{
font-weight: 700,
color: 'green'
}}
/>);
// Higher Order Components
const EnhancedHello1 = withNameOverride(HelloComponent);
const EnhancedHello2 = withStyling(HelloComponent);
const EnhancedHello3 = compose(withNameOverride, withStyling)HelloComponent);

In this snippet, HelloComponent is a component that we can enhance with withNameOverride and/or withStyling.

<EnhancedHello1 name='World' /> Becomes <div>Hello New Name</div>
 <EnhancedHello2 name='World' />
Becomes <div style={{...}}>Hello World</div>
<EnhancedHello3 name='World' /> Becomes <div style={{...}}>Hello New Name</div>

These are simple examples to show how higher-order components can help with writing code with a high degree of composability. When many people think about higher-order components, it’s hard not to mention Recompose, a library of very reliable enhancers. With ~1.4 million downloads a week, it's safe to say that it is an incredibly popular library purely building off the pattern of HoCs. It demonstrates some of the best potentials for the usage of HoC’s.

For many React devs, HoCs are a quintessential part of the ecosystem. At least this was the mindset leading into October 2018, when React Conf was held. It was there that the React team unveiled Hooks.

Being built in part with the creator of Recompose, hooks were heralded as the superior solution to HoCs in every way. Even the Recompose readme was updated by its creator to recommend people to move towards 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 [Recompose]

This statement was strong and divisive to many devs who had gotten very used to Recompose (myself included). We had built large scale applications around the compostability of HoCs and it was hard to believe that hooks could magically replace all of that. This made me more curious as to whether there is still any merit left to using HoCs in new React development. I’ve outlined a few of my key usages of HoCs and how they stack up in a post-hooks world.

Component Local State

I love writing functional components as much as possible. Class components can become unpredictable and difficult to maintain as they grow in size. Recompose offered an enhancer to compose local state into functional components in a way that let me not need to compromise how I prefer to write components. Hooks are designed for use in functional components right from the start and in this battle, hooks changed the game. The useState hook allows all the state management logic I need while being incredibly concise and elegant to read and write. In every case where I used to use withState, I have exclusively switched to useState.

Component Lifecycles

Once again, a clear win for hooks. useEffect simplifies the understanding necessary in order to add side effects to your components. Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint, during a deferred event. This makes it suitable for the many common side effects, like setting up subscriptions and event handlers, because most types of work shouldn’t block the browser from updating the screen. Additionally useEffect offers an easy way to write cleanup code which helps on component rerenders with cleaning up a previous effect before the next effect is performed. Recompose and withLifecycle offer HoC solutions, but without the streamlined usage that useEffectoffers. I have yet to run into limitations with this hook that lifecycle HoC’s could solve better.

Testability

One of the things that made me gravitate towards HoC’s early on was how modular they can be. In that modularity, they are highly unit testable. However, hooks are not the love child of a single ill-conceived night of passion. They are a tool that people have put a lot of careful thought into. One of these aspects of thought is indeed testing. There are fantastic libraries out there being actively maintained which serve the purpose of testing hooks and testing them well. In this aspect, both patterns can be equally testable in many ways, however, it comes down to specific cases where merit can be proven.

Branching Renders

Consider this case: 
You have a component that takes a prop value barand supplies it to a function that returns{ results, loading, error }from some request side effect. When loading is defined, you need to render a loading indicator component. When error is defined, you need to render an error component. It’s only when neither loading or error are defined that you can assume result to be valid.

A hooks implementation could look something like this (assume useRequest is a nontrivial function that uses hooks to perform the request):

export const Foo = ({ bar }) => {
const { result, loading, error } = useRequest(bar);
if (error) {
return <ErrorComponent />;
} else if (loading) {
return <LoadingComponent />;
}
return <ResultComponent result={result} />;
};

This looks reasonably good at a glance, but what about when we have to repeat this pattern for a dozen different requests? You would copy and paste this conditional block to each of those different implementations and in each of them, it would need to be tested on its own.

An HoC implementation could resemble something like this:

const Foo = ({ result }) => <ResultComponent result={result} />;
const withRequest = BaseComponent => ({ bar, ...props }) => {
const { result, loading, error } = useRequest(bar);
return (
<BaseComponent {...props} result={result} loading={loading} error={error} />
);
};
const withError = branch(({ error }) => error, ErrorComponent);
const withLoading = branch(({ loading }) => loading, LoadingComponent);
export default compose(
withRequest,
withLoading,
withError
)(Foo);

While it definitely is more lines, pay attention to the modularity of each constant. branchis an enhancer from Recompose that accepts a test function and two higher-order components. The test function is passed the props from the owner. If it returns true, the left component is applied, otherwise, the right component is applied. When we compose all of these pieces together, the behavior is equivalent to before. When we consider the same hypothetical as before and think about how to test this pattern for a dozen similarly structured requests, the benefits become apparent. We need to test withError and withLoading once and then we can reuse this logic as many times as we want while still maintaining the same test coverage without the need for writing new tests.

Prop Mutation

Consider this case: 
You have a component that takes a prop foo but needs it transformed into bar before use. After consulting the Hooks API reference for guidance, I genuinely do not see a way that hooks can solve this problem (If you happen to know how to do this with hooks, please comment and let me know). However, here is the HoC approach:

const Result = ({ bar }) => <ResultComponent result={bar} />;
const withBar = BaseComponent => ({ foo, ...props }) => (
<BaseComponent {...props} bar={transform(foo)} />
);
export default withBar(Result);

And even in this case withProps could replace much of the implementation of withBar. However, I chose to be more explicit here for clarity.

Conclusion

Comparing HoC’s vs hooks is a naive argument. It’s true that hooks manage to more-elegantly solve some of the large challenges that were once only handled through HoC’s (with recompose). However, if you look closer, then it becomes apparent that HoC’s still has a place in React.

In my Branching Renders example for HoCs, I intentionally use the same useRequest function that was used in the hooks equivalent example. Did you notice that?. It would be compatible in either case. The purpose here is to show that hooks can handle a lot of these big concepts like state and lifecycles really well, but the code that uses it can also benefit heavily from the compostability and modularity of HoC’s.

The impact of the Recompose author’s statements regarding hooks is definitely bold, but not completely factual. If anything, it should be this.

Hooks solves many of the main problems I attempted to address with Recompose three years ago. Because of this, some of the utilities are less useful now and I would recommend checking out hooks.

So to bring this back to the title of this article, it’s Recompose that has less of a role in a React Hooks world, but at least for now, higher-order components are here to stay.

*In this article, I touched on a few different aspects between hooks and HoC’s, but definitely not all of them. Refs and Context are some that come to mind. I would be remiss to not mention them at all, but I could not figure out good examples to illustrate between the two patterns. One of the fantastic side effects from the creation of React hooks is that it confronts developers with a question: What other kinds of things do you find more useful or valuable as an HoC’s rather than with React hooks and vice versa?