How to Use the Fetch API With React Higher Order Components

Fetching data for a website is a common use case so it's a good example for exploring the different ways we can reuse common functionality in a React app.

Our components will be able to fetch data without knowing the implementation details, so the Fetch API code could be replaced in future with Axios, GraphQL or some other data service without modifying the components that consume the fetch.

Common code reuse in React can be implemented using Higher Order Components, Render Props and Hooks. This article will tackle Higher Order Components and more articles will follow on those other methods.

What's a Higher Order Component (HOC)?

Here are the React docs for HOCs (it's well worth taking the time to read through the React Concepts and Advanced Guides properly if you haven't already).

A Higher Order Component is a function that takes a component as an argument and returns that component. This gives us the ability to perform some functionality before returning the component. This functionality can be common and reusable, in our example the fetching of data using the Fetch API.

Let's see the code and then talk about what's going on. We'll name our HOC withFetch.


  // withFetch.js
  import React from 'react';
  
  function withFetch(WrappedComponent, requestUrl) {
    
    class WithFetch extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          data: []
        };
      }
      
      // fetch functionality to populate this.state.data
      
      render() {
        return (
          <WrappedComponent data={this.state.data} {...this.props} />
        )
      }
    }
    
    return WithFetch; 
  }
  
  export default withFetch;

(If you want to see the HOC returning a functional component, it's at the bottom 👇 of this post).

That's the skeleton for our withFetch HOC. Walking through it...

  • withFetch will receive a component (we've called the parameter WrappedComponent), and a requestUrl for the first data fetch.
  • A new React component named WithFetch is created which has it's own state, lifecycle methods and render method.
  • The WithFetch state contains a data property where we'll store the data from the fetch
  • The WithFetch render method returns the component that was passed in (WrappedComponent), and passes it the data of WithFetch as a prop.
  • The {...this.props} are also passed down through to the WrappedComponent
  • WithFetch is returned.

Where is WithFetch returned to though? How is this called? Let's have a Todos component that receives a list of Todos and outputs them...


  // Todos.js
  import React from 'react';
  
  class Todos extends React.Component {
    
    renderTodos() {
      if (!this.props.data) return;
      
      const todoItems = this.props.data.map(todo => (
        
  • {todo.title}
  • )); const todoList = ( <> <h1>Todos</h1> <ul>{todoItems}</ul> </> ); return todoList; } render() { return <>{this.renderTodos()}; } } export default Todos;

    The Todos component renders the todos if it receives this.props.data.

    So let's render this Todos component, wrapped in the HOC that performs the fetch.

    Here's App.js, a component that displays the Todos component.

    
      // App.js
      import React from 'react';
      import Todos from './Todos.js'
      import withFetch from "./withFetch";
      
      // create the Todos wrapped in the HOC
      const TodosWithFetch = withFetch(
        Todos,
        "https://jsonplaceholder.typicode.com/todos"
      );
      
      function App() {
        return (
          <>
            <TodosWithFetch />
          </>
        );
      }
      
      export default App;
    
    

    Here's what's going on in App.js

    • The Todos component is imported
    • The withFetch function is imported
    • We create a const TodosWithFetch that calls the function withFetch (don't do this within the return or render method of App.js, it'll create a new instance every render)
    • We pass withFetch the Todos component, along with the request URL string (you'd probably want to have your API Urls and paths imported from elsewhere, but I'm just using a string inline here for brevity).
    • As seen above in withFetch, the withFetch function returns a React component which renders the Todos component we passed it, and this is assigned to TodosWithFetch.
    • TodosWithFetch is output in the App component.

    The data fetch is taking place in withFetch using the requestUrl we passed it, so let's perform that fetch now. Back to withFetch...

    
      // withFetch.js
      import React from 'react';
      
      function withFetch(WrappedComponent, requestUrl) {
        
        class WithFetch extends React.Component {
          constructor(props) {
            super(props);
            this.state = {
              data: []
            };
          }
          
          componentDidMount() {
            if (requestUrl) {
              this.fetchData(requestUrl);
            }
          }
          
          fetchData = async (requestUrl) => {
            this.setState({
              data: []
            });
            
            try {
              const response = await fetch(requestUrl);
              
              if (response.ok) {
                const data = await response.json();
                this.setState({
                  data
                });  
              } else {
                throw new Error('Fetch request error');
              }
              
            } catch (err) {
              // handle an error
            }
          };
          
          render() {
            return (
              <WrappedComponent data={this.state.data} {...this.props} />
            )
          }
        }
        
        return WithFetch; 
      }
      
      export default withFetch;
    
    

    We've added two things here, the componentDidMount and fetchData methods.

    • fetchData receives the requestUrl argument
    • In fetchData we reset the data state (we could also initialise other variables here like the loading and error states)
    • ...and wrap the fetch in a try catch block to catch errors
    • Fetch isn't rejected for a 404 or 500 error status so it won't be caught by the try catch. For 404/500s it returns false for response.ok, so we check this to make sure the reponse was successful, otherwise we throw an error to be caught by the try catch.
    • If the response is ok we convert it to json and set the state data property
    • This state change means the data is then passed to the WrappedComponent as the data prop, which in our case renders the Todos component.
    • componentDidMount calls fetchData if requestUrl has a value

    Handling Loading and Error States

    As mentioned above, you can add an isLoading or isError property to the WithFetch state, and set these when the fetch takes place. You can then set them back to true or false on success or error of the fetch.

    
      // withFetch.js
        
      class WithFetch extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            data: [],
            isLoading: false,
            isError: false
          };
        }
        
        // ...
        
        fetchData = async (requestUrl) => {
          this.setState({
            data: [],
            isLoading: true,
            isError: false
          });
          
          try {
            const response = await fetch(requestUrl);
            
            if (response.ok) {
              const data = await response.json();
              this.setState({
                data,
                isLoading: false,
              });  
            } else {
              throw new Error('Fetch request error');
            }
            
          } catch (err) {
            // handle an error
            this.setState({
              isLoading: false,
              isError: true // or pass the err itself
            });
          }
        };
        
        render() {
            return (
              <WrappedComponent 
                {...this.state}
                {...this.props} />
            )
          }
          
        // ...
    
    

    In the constructor the isLoading and isError state properties are set to false. isLoading is set to true when fetchData is called, and false again on fetch success or error. isError is set to false on fetchData call and set to true or false depending on the fetch response. These values are then passed to the WrappedComponent by spreading {...state}, where they can be handled accordingly.

    These multiple state properties could be handled by using the React hook useReducer, but I'll address this in an article for fetching data using Hooks (coming soon!).

    Calling Fetch from the WrappedComponent

    At the moment the fetch takes place on componentDidMount, but what if we want to call a fetch from the wrapped component, based on user input for example?

    We can pass down access to methods on the WithFetch component as we've done with state and props. So our fetching data functionality can be passed as a prop to the WrappedComponent, and called from Todos.js.

    
      // withFetch.js
        
      // ...
        
      render() {
          return (
            <WrappedComponent 
              {...this.state}
              {...this.props}
              getData={(requestUrl) => this.fetchData(requestUrl)}
              />
          )
        }
        
      // ...
    
    

    The getData prop passed to the WrappedComponent is a function that takes a new requestUrl as an argument and calls the fetchData method with that requestUrl.

    This can be used by the Todos component simply by calling the getData prop.

    
      // Todos.js
        
      // ...
        
      render() {
        return (
          <>
            {this.renderTodos()}
            <button
              onClick={() =>
                this.props.getData("https://jsonplaceholder.typicode.com/posts")
              }
            >
              Load Data
            </button>
          >/>
        );
      }
        
      // ...
    
    

    I've added a button to the Todos component that when clicked calls the getData prop function with a new requestUrl. This is a contrived example for brevity and not very useful as the requestUrl is unchanging, but you can see how methods on the HOC can be called, and amend accordingly to more usefully respond to user input.

    Debugging

    The HOC returns a component named WithFetch, but when you're debugging in React dev tools you might like this to be named relative to the component you've passed to the HOC. See the React docs for how to set the displayName property to something more appropriate.

    Using Functional Components

    The withFetch HOC returns a React class component, but we can just as easily return a functional component.

    
      // withFetch.js
      import React, { useEffect, useState } from "react";
      
      function withFetch(WrappedComponent, requestUrl) {
        const WithFetch = (props) => {
          const [data, setData] = useState([]);
          const [isLoading, setIsLoading] = useState(false);
          const [isError, setIsError] = useState(false);
          
          useEffect(() => {
            if (requestUrl) fetchData(requestUrl);
          }, []);
          
          const fetchData = async (requestUrl) => {
            setIsLoading(true);
            setIsError(false);
            
            try {
              const response = await fetch(requestUrl);
              if (response.ok) {
                const data = await response.json();
      
                setIsLoading(false);
                setData(data);
              } else {
                throw new Error("Fetch request error");
              }
            } catch (err) {
              setIsLoading(false);
              setIsError(err);
            }
          };
      
          return (
            <WrappedComponent
              data={data}
              isLoading={isLoading}
              isError={isError}
              {...props}
              getData={(requestUrl) => fetchData(requestUrl)}
            />
          );
        };
      
        return WithFetch;
      }
      
      export default withFetch;
    
    

    Let's run through the main differences between the functional and class components.

    • WithFetch is assigned a function rather than a class
    • We use the useState hook for the holding and setting state, rather than the React class component this.state and this.setState
    • We use the useEffect hook for implementing componentDidMount (note that like componentDidMount this only runs once as it's second argument is an empty array)
    • The previous class methods are changed to functions
    • The props passed to the WrappedComponent are the constants that are returned from the {useState} hook rather than spreading {...this.state} in the class version
    • Using the functional component doesn't require any changes to how the HOC is called from App.js

    Other Fetching Data in React Articles

    Check back if you're interested in using Render Props or React Hooks, I'll be adding them to the blog soon.

    If you have any questions or comments or want to connect, you can follow me on Twitter, or sign up to the newsletter in the footer for front-end articles to your inbox 👇


    Back home