Support both Hooks and Render Props with One Simple Trick
Share logic in your React package with support for both custom Hooks and class components by using the Hydra pattern.
Here’s a scenario I’d like you to consider…
You are the author of the wildly popular npm package awesome-counter
. It supports all the logic
necessary to build an up/down counter. It holds the count in state and provides functions to
increment and decrement the count. And, because you are a good citizen, and want to give control of
how things are rendered to the consumer of the component, you of course support render props.
A sample implementation is shown here.
<Counter>
{({ count, incCount, decCount }) => (
<>
<h1>{count}</h1>
<button onClick={decCount}>Count Down</button>
<button onClick={incCount}>Count Up</button>
</>
)}
</Counter>
That’s great! It’s easy to use and flexible.
But what’s all that talk about render props being dead, now that Hooks are a thing in React 16.7?
Note: Hooks is an experimental technology proposed by the React core team and available for use in React 16.7.0-alpha but are NOT production ready.
You can add support for Hooks, but you can’t just bump the major version number on awesome-counter
and drop support for render props. Well, you could, but you’d have hundreds of thousands of unhappy
users who rely on render props support.
There has to be a way to support both components with render props and the new custom Hooks. Well there is, and I call it a Hydra Component.
What is the Hydra pattern?
A hydra is a mythical creature with multiple heads. A package that follows the Hydra pattern contains all of the logic and state, and exports both a custom Hook, as well as a component that accepts a render prop. It too has multiple heads, if you will.
Here are the requirements for a Hydra package:
- Export a React component as
default
. - The exported component uses a render prop.
- Export a custom Hook as a named export. The name must begin with
use
.
Converting to a Hydra package
Here are the steps to convert a standard component that accepts a render prop to a Hydra component.
First let’s take a look at the code of our awesome-counter
component.
import { Component } from 'react';
import renderProps from 'render-props';
class Counter extends Component {
state = { count: this.props.initialCount };
deltaCount = delta => this.setState(({ count }) => ({ count: count delta }));
render() {
const { count } = this.state;
const { children, render = children } = this.props;
const props = {
count,
incCount: () => this.deltaCount(1),
decCount: () => this.deltaCount(-1),
};
return renderProps(render, props);
}
}
Counter.defaultProps = {
initialCount: 0,
};
export default Counter;
If you’ve been coding in React for any time at all, the code above should look pretty familiar. We
set up state
to hold our count
and update it when someone calls incCount
or decCount
. They
are all passed as props
to the render prop function or component.
Now let’s extract just the logic portion of the code above and implement it as a custom Hook.
import { useState } from 'react';
const useCounter = initialCount => {
const [count, setCount] = useState(initialCount);
const deltaCount = delta => setCount(count => count delta);
return {
count,
incCount: () => deltaCount(1),
decCount: () => deltaCount(-1),
};
};
export { useCounter };
Now that’s impressive! Notice how the same logic can implemented with so few lines of code. Because it’s a custom Hook, it’s just a function and not a component. Remember, Hooks don’t render anything—they are strictly concerned with the data. The component that uses the Hook will handle all aspects of rendering.
But what if we wanted to export both a Counter
component and useCounter
? We wouldn’t want the
two pieces of code to sit side-by-side. That would mean duplicating code in both implementations,
which would be a maintenance nightmare.
Instead, we can have our Counter
component call the useCounter
Hook, then pass the data to the
render prop, like this.
const Counter = ({ initialCount, children, render = children }) =>
renderProps(render, useCounter(initialCount));
That’s very little overhead with no code duplication. It’s almost too easy.
The entire implementation of our awesome-counter
that supports the useCounter
custom Hook and
Counter
component is as follows.
import { useState } from 'react';
import renderProps from 'render-props';
const useCounter = initialCount => {
const [count, setCount] = useState(initialCount);
const deltaCount = delta => setCount(count => count delta);
return {
count,
incCount: () => deltaCount(1),
decCount: () => deltaCount(-1),
};
};
const Counter = ({ initialCount, children, render = children }) =>
renderProps(render, useCounter(initialCount));
Counter.defaultProps = {
initialCount: 0,
};
export default Counter;
export { useCounter };
The TL;DR of it all
To convert your existing render props component package into one that supports both a custom Hook and a component with render props, you:
- Convert your existing component’s logic to a custom Hook.
- Export the custom Hook as a named export, prefixed with
use
. - Create a function component that uses the custom Hook and calls a render prop.
- Export the component as
default
.
So just like the mythical Hydra creature, our awesome-counter
package is multi-headed and can
support our new Hook savvy users, as well as our current customer base that uses render props.
Important Notice: Opinions expressed here are the author’s alone. While we're proud of our engineers and employee bloggers, they are not your engineers, and you should independently verify and rely on your own judgment, not ours. All article content is made available AS IS without any warranties. Third parties and any of their content linked or mentioned in this article are not affiliated with, sponsored by or endorsed by American Express, unless otherwise explicitly noted. All trademarks and other intellectual property used or displayed remain their respective owners'. This article is © 2018 American Express Company. All Rights Reserved.