Share logic in your React package with support for both custom Hooks and class components by using the Hydra pattern.

Hercules Slaying the Hydra, From the Labors of Hercules

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:

  1. Export a React component as default.
  2. The exported component uses a render prop.
  3. 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:

  1. Convert your existing component’s logic to a custom Hook.
  2. Export the custom Hook as a named export, prefixed with use.
  3. Create a function component that uses the custom Hook and calls a render prop.
  4. 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.