loading...
Cover image for Refactoring Higher-Order Components (HOC) to React Hooks

Refactoring Higher-Order Components (HOC) to React Hooks

gethackteam profile image Roy Derks Updated on ・8 min read

With the release of React version 16.8, also labelled "The One With Hooks", the long awaited Hooks pattern was introduced. This patterns let's you use state, lifecycles and (almost) any other React feature without the use of classes. If you've been working with React for a longer period, this either felt like a big relief or a giant shock. To me it felt as a relief, as I already preferred using function components over class components. To avoid having to deal with too many class components, some of the projects I'm working on are reusing class logic using Higher-Order Components (HOC) - which can get quite complex. In this post I'll be converting one of these HOCs to a custom Hook, to demonstrate the power of this "new" pattern.

Sidenote: You can use classes or Hooks depending on your own preference as there are no breaking changes for the use of classes scheduled - yet.
And when you're reading this article you probably already have tried any of the Hooks or at least have read a lot about it. In case you haven't, this overview in the official React documentation is a great place to start

Higher-Order Components (HOC)

As briefly mentioned before a HOC is a pattern to reuse (class) component logic across your React application. That way you don't have to duplicate logic that's in example based on state updates, like data fetching or routing. The React docs describe a HOC as a "function that takes a component and returns a new component", roughly meaning the component that is used as input for the HOC will be enhanced and returned as a different component. HOCs are very commonly used in React by packages like react-router or react-redux. Examples of HOCs in these packages are the withRouter and connect HOCs. The first one lets you access routing props in any component that you pass to it, while the latter makes it possible to connect to the Redux state from the input component.

Creating a HOC isn't that hard and is very well explained in the documentation on the official React website, which I'll demonstrate by creating a new HOC that's called withDataFetching. This will add basic data fetching features using state and lifecycles to any component that you pass to this HOC. Using the Github API a component will be created that renders a list of my public repositories.

  • The starting point is create a function that takes a component as input and returns a different component based on this component. This function does nothing more than construct a new class component WithDataFetching that returns the input component WrappedComponent.
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {

    render() {
      return (
        <WrappedComponent />
      );
    }
  }

  return WithDataFetching;
};

export default withDataFetching;
  • After which you can add the data fetching logic to this function, by using state and lifecycles. In the constructor() the initial state values are set, while the data fetching is done in in the asynchronous componentDidMount() lifecycle using the fetch() method.
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {
    constructor() {
      super();
      this.state = {
        results: [],
        loading: true,
        error: ""
      };
    }

    async fetchData() {
      try {
        const data = await fetch(props.dataSource);
        const json = await data.json();

        if (json) {
          this.setState({
            results: json,
            loading: false
          });
        }
      } catch (error) {
        this.setState({
          loading: false,
          error: error.message
        });
      }
    }

    async componentDidMount() {
      this.fetchData();
    }

    // ...
  }

  return WithDataFetching;
};

export default withDataFetching;
  • In the render() method the WrappedComponent is returned and the state values loading, results and error should be passed to it as props. That way the results returned by the data fetching will become available on the input component.
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {
    // ...

    render() {
      const { results, loading, error } = this.state;

      return (
        <WrappedComponent
          results={results}
          loading={loading}
          error={error}
          {...this.props}
        />
      );
    }
  }

  return WithDataFetching;
};

export default withDataFetching;
  • And finally you can set the display name of the component that's being returned by the HOC, as otherwise this new component is hard to track in example the React DevTools. This can be done by setting the displayName of the WithDataFetching component.
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {
    // ...

    render() {
      // ...
    }
  }

  WithDataFetching.displayName = `WithDataFetching(${WrappedComponent.name})`;

  return WithDataFetching;
};

export default withDataFetching;

This created the HOC that can be used to add data fetching features to any component that is passed to this function. As you can see this HOC is setup as a curried function, meaning it will take several arguments. Therefore you cannot only pass a component as a parameter, but also other values as a second parameter. In the case of the withDataFetching HOC you can also send an object containing props for the component, where the prop dataSource is used as the url for the fetch() method. Any other props that you'll pass in this object will be spread on the WrappedComponent that is returned.

  • In a function component that's called Repositories the withDataFetching HOC component must be imported. The default export of this file is the HOC component that takes the Repositories component and an object containing the field dataSource. The value of this field is the url to the Github API to retrieve the repositories for a username.
import React from "react";
import withDataFetching from "./withDataFetching";

function Repositories() {

  return '';
}

export default withDataFetching({
  dataSource: "https://api.github.com/users/royderks/repos"
})(Repositories);
  • As the HOC adds data fetching capabilities to the Repositories component, the props loading, results and error are passed to this component. These result from the state and lifecycle values in withDataFetching, and can be used to display a list of all the repositories. When the request to the Github API has not resolved yet or an error occurs, a message will be displayed instead of the repositories list.
import React from "react";
import withDataFetching from "./withDataFetching";

function Repositories({ loading, results, error }) {
  if (loading || error) {
    return loading ? "Loading..." : error.message;
  }

  return (
    <ul>
      {results.map(({ id, html_url, full_name }) => (
        <li key={id}>
          <a href={html_url} target="_blank" rel="noopener noreferrer">
            {full_name}
          </a>
        </li>
      ))}
    </ul>
  );
}

export default withDataFetching({
  dataSource: "https://api.github.com/users/royderks/repos"
})(Repositories);

With this last change the Repositories is able to display the results from the data fetching that's done in the HOC. This can be used for any endpoint or component, as HOCs make reusing logic easy.

In this CodeSandbox below you can see the results of passing the Repositories component to the HOC:

Custom Hooks

In the introduction of this post I told that Hooks make it possible to use React features, like state, outside class components. To correct myself: Hooks can only be used in function components. Also, by building custom Hooks you can reuse the data fetching logic from the previous HOC in almost the same matter. But first let's have a brief look into Hooks, and specifically the useState() and useEffect() Hook.

  • The useState() Hook let's you handle state from any function component, without having to use a constructor() and/or this.setState() method.

  • The useEffect() Hook is the equivalent of both the componentDidMount() and componentDidUpdate() lifecycle method. Using just this Hook you can watch for updates of specific (state) variables or no variables at all.

If you aren't familiar with these Hooks yet this might sound confusing, but lucky for you you'll use both Hooks to create a custom useDataFetching() Hook. This Hook will have the same data fetching logic as the withDataFetching HOC, and call the Github API using the fetch() method. The Hook will return the same values as the HOC, which are loading, results and error.

  • First you need to create the function useDataFetching that takes the parameter dataSource, this parameter is the url that needs to be fetched later on. This custom Hook needs react as a dependency as you want to use React features, from where you import the two Hooks you'll be using.
import React, { useState, useEffect } from "react";

function useDataFetching(dataSource) {

  return {};
}

export default useDataFetching;
  • The Hook should return the values loading, results and error; these values must be added to the state of this Hook and returned afterwards. Using the useState() Hook you can create these state values, and also a function to update these values. But first create the state values and return them at the end of this useDataFetching function.
import React, { useState, useEffect } from "react";

function useDataFetching(dataSource) {
  const [loading, setLoading] = useState(true);
  const [results, setResults] = useState([]);
  const [error, setError] = useState("");

  return {
    loading,
    results,
    error
  };
}

export default useDataFetching;

The initial values for the return values are set when calling the useState Hook, and can be updated using the second value of array that's being returned by the Hook. The first value is the current state value and should therefore be returned at the end of the custom Hook.

  • In the withDataFetching HOC there was a function to send a request to the Github API called fetchData. This function must also be added to the custom Hook. The only difference is that the state values aren't updated using the this.setState() method, but by calling the update functions returned by the useState() Hooks. This fetchData function must be put inside the useEffect() Hook, which allows you to control when this function is being called.
import React, { useState, useEffect } from "react";

function useDataFetching(dataSource) {
  const [loading, setLoading] = useState(true);
  const [results, setResults] = useState([]);
  const [error, setError] = useState("");

  useEffect(() => {
    async function fetchData() {
      try {
        const data = await fetch(dataSource);
        const json = await data.json();

        if (json) {
          setLoading(false);
          setResults(json);
        }
      } catch (error) {
        setLoading(false);
        setError(error.message);
      }

      setLoading(false);
    }

    fetchData();
  }, [dataSource]);

  return {
    error,
    loading,
    results
  };
}

export default useDataFetching;

In the code block above the fetchData function is called when the value for dataSource is updated, as this value is added to the dependency array for the useEffect() Hook.

From a function component you're now able to call the custom useDataFetching() Hook to use the data fetching values in that component. Different then for the HOC these values aren't added as props to the component, but returned by the Hook.

  • In a new function component called RepositoriesHooks you need to import useDataFetching() and destructure the values for loading, results and error from the result returned from this Hook. The url to retrieve all the repositories of a user from the Github API should be added as a parameter.
import React from "react";
import useDataFetching from "./useDataFetching";

function RepositoriesHooks() {
  const { loading, results, error } = useDataFetching("https://api.github.com/users/royderks/repos");

  return '';
}

export default RepositoriesHooks;
  • To display the repositories in a list you can copy the return values from the Repositories components, as nothing has changed except for the manner in which the values for loading, results and error are added in this component.
import React from "react";
import useDataFetching from "./useDataFetching";

function RepositoriesHooks() {
  const { loading, results, error } = useDataFetching(
    "https://api.github.com/users/royderks/repos"
  );

  if (loading || error) {
    return loading ? "Loading..." : error.message;
  }

  return (
    <ul>
      {results.map(({ id, html_url, full_name }) => (
        <li key={id}>
          <a href={html_url} target="_blank" rel="noopener noreferrer">
            {full_name}
          </a>
        </li>
      ))}
    </ul>
  );
}

export default RepositoriesHooks;

By creating the custom useDataFetching Hook you're now able to use data fetching in any function component using React Hooks instead of by creating a HOC. If you want to see the changes affected in the CodeSandbox you need to comment out the import of the Repositories component in src/index.js and import the RepositoriesHooks component instead.

import React from "react";
import ReactDOM from "react-dom";

// import Repositories from "./Repositories";
import { default as Repositories } from "./RepositoriesHooks";

function App() {
  return (
    <div className="App">
      <h1>My Github repos</h1>
      <Repositories />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Summary

The new Hooks pattern makes it possible to use state, lifecycles and other features from React outside of class components. Earlier, you were only able to use these features in class components and therefore needed Higher-Order Components (HOC) to reuse any of this logic you put in there. From React version 16.8 onwards you're able to use Hook to access React features like state from function components. By creating custom Hooks, such as the useDataFetching() Hook above, you can reuse in example state logic from any function component.

Hopefully this post helps you deciding whether or not you should convert any of your HOCs to a custom Hook! Don't forget to leave any feedback, or follow me on Twitter to stay up-to-date 😄!

Discussion

pic
Editor guide
Collapse
amitnovick profile image
Amit Novick

One quality that I like about HOC's that Hooks don't have is being externally composable.

What I mean is that components wrapped by an HOC don't need to be aware of it. They receive props and use those props as the one and only interface, and are therefore decoupled from the HOC: indeed, you can even take out the HOC and replace it with any other component that preserves the props interface, and the component need not change.

Meanwhile a Hook must be embedded inside a component in order to be used by it, which makes the component coupled to that Hook's implementation. If you now want to replace that hook with something else, you must now change every component that uses it, since there is no interface to satisfy. It's true that hooks are a function which also represent an interface (accept input, return output), but one of the biggest selling point of React is being able to use the props interface, and Hooks just seem lower level and not as composable.

Feel free to share your thoughts on this, would love to hear other valid perspectives.

Collapse
jaimesangcap profile image
Jaime Sangcap

Isn't it the same when using HOC? When HOC change, you have to replace all calls to it? Well it just happens not inside the component. But it's most likely on the same file.

Collapse
chrisachard profile image
Chris Achard

Nice overview! You've hit on one of the places that I think hooks are much more understandable (especially for beginners!) - because I've seen HOCs cause a lot of confusion.

Also: I never realized that github had an api like that! (of course it does though 😄) - but that's a great api to use for demos; thanks!

Collapse
gethackteam profile image
Roy Derks Author

Thank you! Yes HOC are often hard to get when you just started with React

Collapse
wierdorohit123 profile image
rohit raut

Thanks Roy it was a great!!! Article

Collapse
wierdorohit123 profile image
rohit raut

One open ques for everybody since i have been working on REACT for past couple of days I'm quite confused with the following. can someone tell me if its a bad practice to use document.querySelector for getting DOM elements in REACT or this should be done only by using ref??? How to handle when we want to capture many elements ?

Collapse
davidkutas profile image
davidkutas

what happens in a situation when you already bound your API call(s) into a useEffect method - in the same way as you write it, but later you need to call the previously set up API call inside yet another usEffect? It will throw an error, because useEffect can only be used in the top of the component; however I do not find the solution how to avoid issues like this.

looking forward to your insights.

Collapse
tankeller profile image
Tan Keller

This Post realy helped me understand the idea of customHooks and HOC.
Thank you!