Fetch data in React

When I tested React for the first time, think it was 2015, I started with simple applications that did not use remote data - instead I created data models locally in my JavaScript code because I wanted to stay focused on how state and prop worked in React. I did not want to add more complexity spending time to choose libraries etc.

This article will cover howe to fetch data to your React app and instead of using state management libraries such as Redux or MobX, we will use React local state.

Fetching data in React

When decide to where in your component tree you are going to fetch data we usually need to answer some question first:

  • What components in your component trees are interested in the data?
  • Where in your application do you want to show a loading indicator (spinner, progress bar, etc)?
  • Where do you want to show error if the fetching fails?

However, we are not going to discuss all possible solutions to these questions since it depends. It depends on your solutions requirements and there is no silver bullet that solves every case available.

How to Fetch data

React's ES6 class components have a lifecycle method called componentDidMount() that is perfect to fetch data. When this method runs, the component has already rendered once (the initial render phase - when React figure out what this component wants to render) - but it will render again if it's local state gets updated.

In the following example we are using the native fetch API:

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);
    // Local state is set in the constructor
    this.state = {
      data: null,
    };
  }

  componentDidMount() {
    // Fetch data from an api endpoint
    // and update local state when done
    fetch('https://api.mydomain.com')      .then(response => response.json())
      .then(data => this.setState({ data }));
  }
}

export default App;

In the next step, we start to abstract common logic for the API endpoint (lines 4-5), initializing the local state (lines 12-13), and adding the render method (line 29).

import React, { Component } from 'react';

// API endpoint to Hacker News
const API = 'https://hn.algolia.com/api/v1/search?query=';const DEFAULT_QUERY = 'redux';
class App extends Component {
  constructor(props) {
    super(props);

    // Initialize the local state with an empty array
    this.state = {      hits: [],    };
  }

  // When fetch is completed we update local state
  // and React will render again to display the data
  componentDidMount() {
    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits }));  }
  
  // Render data
  // If hits from the local state is an empty
  // array, it will not render because the map
  // method of an array returns an empty array    
  render() {    const { hits } = this.state;
    return (
      <ul>
        {hits.map(hit =>
          <li key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        )}
      </ul>
    );
  }
}
export default App;

When the data is fetched successfully, it will be stored in the local state with React's this.setState() method line 22. Updating state will trigger React to render again and since the state now holds the fetched data, the render method will render a simple HTML list of news from Hacker News.

Even though the render() method already ran once before the componentDidMount() method, you don't run into any null

pointer exceptions because you have initialized the property in the local state with an empty array.

Loading and error state?

Now when we know how to fetch data and store it in local state, we need to consider loading and error properties that we are going to store in the local state. Handle loading and error will improve your users experience of you application.

If we consider the lifecycle methods componentDidMount() and render(), we realize there is a gap between the initial render phase and, the next render phase - after the local state has been updated. That gap would be a perfect time to render a loading indicator to the end-user.

Let's implement a loading indicator in the local state, you will find comments inside the code about what's going on:

import React, { Component } from 'react';

// API endpoint to Hacker News
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';

class App extends Component {
  constructor(props) {
    super(props);

    // We add a boolean to indicate loading state 
    this.state = {
      hits: [],
      isLoading: false,    };
  }

  // When fetch is completed we update local state
  // and React will render again to display the data
  componentDidMount() {

    // Before fetching we set the loading indicator to true
    this.setState({ isLoading: true });  
    // When fetch has new data, update state and set loading
    // indicator to false.
    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits, isLoading: false }));  }
  
  // Render data
  render() {
    const { hits, isLoading } = this.state;

    // Render the loading indicator
    if (isLoading) {      return <p>Loading ...</p>;    }
    return (
      <ul>
        {hits.map(hit =>
          <li key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        )}
      </ul>
    );
  }
}
export default App;

When doing software development, we usually spend most of our time figure out how to handle errors and, we need to take care of the error state if the fetching does not work. In this example we chose to handle the error in the local state and below we can se how to implement it.

import React, { Component } from 'react';

// API endpoint to Hacker News
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';

class App extends Component {
  constructor(props) {
    super(props);

    // We add an error prperty in the state
    this.state = {
      hits: [],
      isLoading: false,
      error: null,      };
  }

  // When fetch is completed we update local state
  // and React will render again to display the data
  componentDidMount() {

    // Before fetching we set the loading property to true
    this.setState({ isLoading: true });

    // We handle error responses from fetch API and throwing
    // an error to be catched if the response was not ok.
    fetch(API + DEFAULT_QUERY)
      .then(response => {
        if(response.ok) {          return response.json();        } else {          throw new Error('Something went wrong ...');        }      })
      .then(data => this.setState({ hits: data.hits, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false }));  }
  
  // Render data
  render() {
    const { hits, isLoading, error } = this.state;

    // Render an error
    if (error) {      return <p>{error.message}</p>;
    }

    // Render the loading indicator
    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <ul>
        {hits.map(hit =>
          <li key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        )}
      </ul>
    );
  }
}
export default App;

Note: fetch API doesn't use its catch block for every erroneous status code. For instance, when HTTP 404 happens,

it wouldn't run into the catch block, but you can force it to run into the catch block by throwing an error when your response doesn't match your expected data, see line 33.

Fetch data with axios

You can substitute the native fetch API with another library if you want, in our example we are using Axios. Axios has features the native fetch API does not have, it allows cancelling requests, centralize and customize error handling, use interceptors to deal with http headers (auth tokens, api token, etc), and it runs for every erroneous requests into the catch block on its own without you having to throw an error in the first place.

The same example as above but using axios instead:

import React, { Component } from 'react';
import axios from 'axios'

// API endpoint to Hacker News
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hits: [],
      isLoading: false,
      error: null,  
    };
  }

  componentDidMount() {
    this.setState({ isLoading: true });

    axios.get(API + DEFAULT_QUERY)      .then(result => this.setState({        hits: result.data.hits,        isLoading: false      }))      .catch(error => this.setState({        error,        isLoading: false      }));  }
  
  render() {
    const { hits, isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <ul>
        {hits.map(hit =>
          <li key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        )}
      </ul>
    );
  }
}
export default App;

As you can see in lines 21-29, axios returns a JavaScript promise, we do not need to resolve the promise two times because axios already returns a JSON response for you. Furthermore, when using axios you can be sure that all errors are caught in the catch() block. In addition, you need to adjust the data structure slightly for the returned axios data.

So far we have described how to automatically fetch data to a component, store the data in the local state and taken care of loading and error handling. We have solved this mini app or component's state management.

What about if the user should be able to interact and trigger the fetching of the data?

In that case we are not using the lifecycle method componentDidMount(), instead we need to add our own class method and wire up an event handler to run our class method that implements the fetching logic.

import React, { Component } from 'react';
import axios from 'axios'

// API endpoint to Hacker News
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hits: [],
      isLoading: false,
      error: null,  
    };
  }

  getNews = event => {    this.setState({ isLoading: true });    axios.get(API + DEFAULT_QUERY)      .then(result => this.setState({        hits: result.data.hits,        isLoading: false      }))      .catch(error => this.setState({        error,        isLoading: false      }));      }    
  render() {
    const { hits, isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <div>
        <button onClick={getNews}>Get news</button>          <ul>
          {hits.map(hit =>
            <li key={hit.objectID}>
              <a href={hit.url}>{hit.title}</a>
            </li>
          )}
        </ul>
      </div>
    );
  }
}
export default App;

In line 44 we are adding our event handler and ensure it calls our class method on the onClick event of a button and, our class method is responsible to implement the fetch logic.

Fetch data using Async/Await

So far, we have only used JavaScript promises by using their then() and catch() blocks but if you want to use async/await instead - the following example will show you how to do.

import React, { Component } from 'react';
import axios from 'axios'

// API endpoint to Hacker News
const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hits: [],
      isLoading: false,
      error: null,  
    };
  }

  async getNews = event => {    this.setState({ isLoading: true });

    try {      const result = await axios.get(API + DEFAULT_QUERY);      this.setState({
        hits: result.data.hits,
        isLoading: false
      });
    } catch (error) {      this.setState({
        error,
        isLoading: false
      });
    }    
  }  
  
  render() {
    const { hits, isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <div>
        <button onClick={getNews} >Get new</button>  
        <ul>
          {hits.map(hit =>
            <li key={hit.objectID}>
              <a href={hit.url}>{hit.title}</a>
            </li>
          )}
        </ul>
      </div>
    );
  }
}
export default App;

Instead of then() you can use the async/await statements when fetching data in React. The async statement in line 18 is used to signalize that a function is executed asynchronously. It can be used on a lifecycle methods too. The await statement is used within the async function whenever something is executed asynchronously, line 22. So the next line of code is not executed before the awaited request resolves. Furthermore, a try and catch block can be used to catch the error in case the request fails, lines 21 and 27.


Published: 2019-12-03
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB