DEV Community 👩‍💻👨‍💻

Cover image for Use Hooks In Class Components Too
Yurui Zhang
Yurui Zhang

Posted on

Use Hooks In Class Components Too

With the official release of hooks, everybody seems to be writing function components exclusively, some even started refactoring all of their old class components. However class components are here to stay. We can't use hooks everywhere (yet), but there are some easy solutions.

Higher Order Components

Higher Order Components (or HOCs) are functions that takes a Component in its arguments, and returns a Component. Before hooks, HOCs are often used to extract common logic from the app.

A simple HOC with a useState hook looks like this:

const withFoo = (Component) => {
  function WithFoo(props) {
    const [foo, setFoo] = useState(null);

    return <Component foo={foo} setFoo={setFoo} {...props} />
  }

  WithFoo.displayName = `withFoo(${Component.displayName})`;

  return WithFoo;
};
Enter fullscreen mode Exit fullscreen mode

Here, our withFoo function, can be called with a Component. Then, it returns a new Component that receives an additional prop foo. The WithFoo (note the capitalized With) is actually a function component - that's why we can use Hooks!

A few quick notes before we move on:

  • Personally I usually name my HOCs with*, just like we always use the pattern use* for hooks.
  • Setting a displayName on the HOC is not necessary, but it is very helpful for debugging your app in react-devtools
  • Usually I spread the original props last - this avoids overwriting props provided by the users of the component, while allowing the users to override the new fields easily.

Our Custom Hook

How do apply this to our useGet hook?

Let's replace useState from the example above to useGet ... but wait, useGet needs to be called with { url } - where do we get that? 🤔

For now let's assume the url is provided to the component in its props:

const withGetRequest = (Component) => {
  function WithGetRequest(props) {
    const state = useGet({ url: props.url });

    return <Component {...state} {...props} />
  }

  WithGetRequest.displayName = `withGetRequest(${Component.displayName})`;

  return WithGetRequest;
};
Enter fullscreen mode Exit fullscreen mode

This works, but at the same time, this means whoever uses our wrapped component will have to provide a valid url in its props. This is probably not ideal because often we build urls dynamically either based on some ids or in some cases, user inputs (e.g. In a Search component, we are probably going to take some fields from the component's state.)

One of the limitations of HOCs is they are often "static": meaning we can't change its behavior easily at run-time. Sometimes we can mitigate that by building "Higher Higher Order Components" (not an official name) like the connect function provided by react-redux:

// connect() returns a HOC
const withConnectedProps = connect(mapStateToProps, mapDispatchToProps);

// we use that HOC to wrap our component
const ConnectedFoo = withConnectedProps(Foo);
Enter fullscreen mode Exit fullscreen mode

So, if our resource's url relies on some fields from the from the props maybe we can build something like this:

// first we take a function that will be called to build a `url` from `props`
const makeWithGetRequest = (urlBuilder) => {
  return withGetRequest = (Component) => {
    return function WithGetRequest(props) {
      const url = urlBuilder(props);
      const state = useGet({ url });

      return <Component {...state} {...props} />;
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

It's safe to assume that different components will have different logic for building the URLs they need. For example, to wrap an ArticlePage component:

// we know articleId and categoryId will be provided to the component
const buildArticleUrl = ({ articleId, categoryId }) => {
  return `/categories/${categoryId}/articles/${articleId}`;
};

// now our enhanced component is using the `useGet` hook!
export default makeWithGetRequest(buildArticleUrl)(ArticlePage);
Enter fullscreen mode Exit fullscreen mode

This seems nice, but it doesn't solve the problem of building url with the component's state. I think we are too fixated on this HOC idea. And when we examine it closely we will discover another flaws with this approach - we are relying on props with fixed names being provided to the component, this could lead to a couple of problems:

  • Name Collision: Users of the enhanced component will have to be extra careful to not accidentally override props provided by HOCs
  • Clarity: Sometimes the prop names are not descriptive. In our ArticlePage example above, the component will receive data and error in its props and it could be confusing to future maintainers.
  • Maintainability: When we compose multiple HOCs, it becomes harder and harder to tell which props must be provided by the user? which props are from HOCs? which HOC?

Let's try something else.

Render Props / Function as Child

Render Props and Function as Child are both very common react patterns and they are very similar to each other.

Render Props is a pattern where a component takes a function in its props, and calls that function as the result of its render (or conditionally, in advanced use cases).

An example with hooks looks like this:

const Foo = ({ renderFoo }) => {
  const [foo, setFoo] = useState(null);

  return renderFoo({ foo, setFoo });
};

// to use it:
class Bar extends Component {
  // ...

  render () {
    return (
      <Foo
        renderFoo={
          ({ foo, setFoo }) => {
            // we have access to the foo state here!
          };
        }
      />
    );
  };
};
Enter fullscreen mode Exit fullscreen mode

When we decide that the user should always provide that render function as children, then we are using the "Function as Child" pattern. Replacing renderFoo with children in our example above will allow us to use it this way:

<Foo>
  {
    ({ foo, setFoo }) => {
      // now we can use foo state here
    }
  }
</Foo>
Enter fullscreen mode Exit fullscreen mode

The two patterns here are often interchangeable - many devs prefer one over the other, and you can even use them at the same time to provide max flexibility, but that'll be a topic for another time.

Let's try this pattern with our useGet hook.

// it takes two props: url and children, both are required.
const GetURL = ({ url, children }) => {
  const state = useGet({ url });

  return children(state); // children must be a function.
};


// now we can use it like this!
class Search extends Component {
  // ...

  render() {
    const { keyword } = this.state;

    return (
      <GetURL url={buildSearchUrl({ keyword })}>
      {
        ({ isLoading, data, error }) => {

          // render the search UI and results here!
        }
      }
      </GetURL>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Easy, right?

Function as Child & Render Props are not without trade-offs. They are more flexible than HOCs but now our original component's JSX is now nested in an inline function - making it a bit tricky to test when using the shallow renderer from enzyme. And what happens if we want to compose multiple hooks in a component? I wouldn't nest another function child inside an existing one.

Wrapping Up

Now we have two ways of making hooks (re-)usable everywhere! If a hook doesn't rely on any dynamic inputs, I would go with the HOC solution; If you want to be more flexible, providing a component with Render Props / Function as Child would be a much better choice.

Next let's talk about testing our hooks & components with jest, sinon and @testing-library/react-hooks. 🎉

Top comments (0)

Want the React badge for your profile?

It's awarded to the top React author each week. Start your post here!