Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Modern API data-fetching methods in React

10 min read 2836

Modern API Data-Fetching Methods In React

Understanding how to fetch data into React applications is mandatory for every React developer who aims to build modern, real-world web applications.

In this guide, we will cover the modern React data-fetching methods and learn how to handle our application’s state while fetching data. Furthermore, we will cover how to handle the application’s state when something goes wrong with the data.

This promises to be interesting. To follow along, ensure you are familiar with React.

Fetching data from an API in a React app

React beginners might wonder, “What exactly is an API?” To understand what an application programming interface (API) is, let’s think of an application where a section displays the daily weather forecast of the present city.

While building this type of app, we can create our backend to handle the weather data logic or we can simply make our app communicate with a third-party system that has all the weather information so we only need to render the data.

Either way, the app must communicate with the backend. This communication is possible via an API, and, in this case, a web API.

As the name implies, the API exposes an interface that our app uses to access data. With the API, we don’t need to create everything from scratch, simplifying our process. We only need to gain access to where the data is located so we can use it in our app.

The two common styles for designing web APIs are REST and GraphQL. While this guide focuses on data fetching from the REST API, the fetching strategies are similar for both. Well, ok… we will also see an example of how we can fetch data from a GraphQL API 😊.

Considerations before fetching data

When we request data, we must prepare a state to store the data upon return. We can store it in a state management tool like Redux or store it in a context object. But, to keep things simple, we will store the returned data in the React local state.

Next, if the data doesn’t load, we must provide a state to manage the loading stage to improve the user experience and another state to manage the error should anything go wrong. This gives us three state variables like so:

We made a custom demo for .
No really. Click here to check it out.

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

Here, we have given the states default values.

The useEffect Hook

When we request to fetch data from the backend, we perform a side effect, which is an operation that can generate different outputs for the same data fetching. For instance, the same request returns a success or error.

In React, we should avoid performing side effects directly within the component body to avoid inconsistencies. Instead, we can isolate them from the rendering logic using the useEffect Hook.

In this case, we will fetch our data in the Hook like so:

useEffect(() => {
 // data fetching here
}, []);

The implementation above will run and fetch data on a component mount, that is, on the first render. This is sufficient for most of our use cases.

In other scenarios, however, when we need to refetch data after the first render, we can add dependencies in the array literal to trigger a rerun of useEffect.

Now that we covered the basics, we can get started with the first fetching method.

Using the JavaScript Fetch API

The Fetch API through the fetch() method allows us to make an HTTP request to the backend. With this method, we can perform different types of operations using HTTP methods like the GET method to request data from an endpoint, POST to send data to an endpoint, and more.

Since we are fetching data, our focus is the GET method.

fetch() requires the URL of the resource we want to fetch and an optional parameter:

fetch(url, options)

We can also specify the HTTP method in the optional parameter. For the GET method, we have the following:

fetch(url, {
  method: "GET" // default, so we can ignore
})

Or, we can simply ignore the optional parameter because GET is the default:

fetch(url)

As mentioned earlier, we will fetch data from a REST API. We could use any API, but here we will use a free online API called JSONPlaceholder to fetch a list of posts into our application; here is a list of the resources we can request

By applying what we’ve learned so far, a typical fetch() request looks like the following:

import { useState, useEffect } from "react";

export default function App() {
 const [data, setData] = useState(null);
 const [loading, setLoading] = useState(true);
 const [error, setError] = useState(null);

 useEffect(() => {
  fetch(`https://jsonplaceholder.typicode.com/posts`)
   .then((response) => console.log(response));
 }, []);

 return <div className="App">App</div>;
}

In the code, we are using the fetch() method to request post data from the resource endpoint as seen in the useEffect Hook. This operation returns a promise that could either resolve or reject.

If it resolves, we handle the response using .then(). But at this stage, the returned data is a Response object, which is not the actual format that we need, although it is useful to check for the HTTP status and to handle errors.

Notice we’ve logged the response. See what we have in return in the console:

Logging The Response Showing A 200 Code And A True OK

Take note of the Response‘s OK status; we will use it later to check for unsuccessful HTTP calls.

Next, we must resolve the Response object to JSON format using the json() method. This also returns a promise and from there, we can resolve to get the actual data that we need:

useEffect(() => {
  fetch(`https://jsonplaceholder.typicode.com/posts`)
    .then((response) => response.json())
    .then((actualData) => console.log(actualData));
}, []);

Now, we have a list of 100 posts fetched from our API. Open the console in this CodeSandbox to see the data.

In case the promise rejects, we will handle the error using the .catch() like so:

useEffect(() => {
 fetch(`https://jsonplaceholder.typicode.com/posts`)
  .then((response) => response.json())
  .then((actualData) => console.log(actualData))
  .catch((err) => {
   console.log(err.message);
  });
}, []);

Note that the promise returned from the fetch() method only rejects on a network failure; it won’t reject if we hit a wrong or nonexisting endpoint like …/postssss. In this case, .catch() will not catch that error, so we must manually handle that.

Earlier we saw how the Response object returns the HTTP status. The OK status is true if we hit the correct endpoint, else it returns false. By checking for that status, we can write a custom error message for a “404 Not Found” like so:

if (!response.ok) {
  throw new Error(
    `This is an HTTP error: The status is ${response.status}`
  );
}

And, the useEffect Hook now looks like this:

useEffect(() => {
  fetch(`https://jsonplaceholder.typicode.com/posts`)
    .then((response) => {
      if (!response.ok) {
        throw new Error(
          `This is an HTTP error: The status is ${response.status}`
        );
      }
      return response.json();
    })
    .then((actualData) => console.log(actualData))
    .catch((err) => {
      console.log(err.message);
    });
}, []);

In the code block, we check if the R``esponse‘s OK status is false, meaning we have a 404 status followed by throwing our custom error message.

When we throw an error in the .then() block, .catch() detects and uses our custom message whenever we hit a “404 Not Found.”

Rendering the posts in the frontend

Presently, we have the posts in the console. Instead, we want to render them in our app. To do that, we’ll first limit the total post number to 8 instead of the 100 posts returned for brevity.

We can do that by appending a query string parameter (?_limit=8) to the request URL:

fetch(`https://jsonplaceholder.typicode.com/posts?_limit=8`)

Next, we must update the state and render the UI:

// ...

export default function App() {
  // ...

  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/posts?_limit=8`)
      .then((response) => {
        // ...
      })
      .then((actualData) => {
        setData(actualData);
        setError(null);
      })
      .catch((err) => {
        setError(err.message);
        setData(null);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  return (
    <div className="App">
      <h1>API Posts</h1>
      {loading && <div>A moment please...</div>}
      {error && (
        <div>{`There is a problem fetching the post data - ${error}`}</div>
      )}
      <ul>
        {data &&
          data.map(({ id, title }) => (
            <li key={id}>
              <h3>{title}</h3>
            </li>
          ))}
      </ul>
    </div>
  );
}

In the code, we update the state data and the error message using the setData and setError, respectively. We also added the .finally block that runs when the promise settles.

This is a good place to cancel the loading effect. Notice that we reset the error and data in the .then() and .catch(), respectively, which prevents inconsistencies for temporary server failure.

See the project on CodeSandbox; we’ve also added styles to improve the visual.

Rendering The API Posts Into The Frontend With Styling

Using the async/await syntax

The previous method explained data fetching using the pure promise syntax. Here we will learn a more elegant method to get data using the async/await.

When we make a request and expect a response, we can add the await syntax in front of the function to wait until the promise settles with the result. But, to use this syntax, we must call it inside the async function in typical JavaScript code.

In the case of fetch``(), the syntax looks like so:

useEffect(() => {
  async function getData() {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts?_limit=10`
    )

    console.log(response)
  }
  getData()
}, [])

In the code, we use the await to wait for the promise from fetch(). Remember, we need the data in json() format, so let’s wait for that as well:

useEffect(() => {
  async function getData() {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts?_limit=10`
    )
    let actualData = await response.json();

    console.log(actualData) 
  }
  getData()
}, [])

Notice we don’t chain .then as we’ve done in the previous method. However, it’s fine to use .then() with async/await:

useEffect(() => {
  async function getData() {
    const actualData = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_limit=10`
    ).then(response => response.json());

    console.log(actualData) 
  }
  getData()
}, [])

In practice, we often use the async/await with a try/catch/finally statement to catch errors and manage the loading state. This is similar to using .then, .catch, and .finally in the previous method:

useEffect(() => {
  const getData = async () => {
    try {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts?_limit=10`
      );
      if (!response.ok) {
        throw new Error(
          `This is an HTTP error: The status is ${response.status}`
        );
      }
      let actualData = await response.json();
      setData(actualData);
      setError(null);
    } catch(err) {
      setError(err.message);
      setData(null);
    } finally {
      setLoading(false);
    }  
  }
  getData()
}, [])

The code above is similar to the previous method. Here, when an error occurs in the try block, the catch statement catches and controls the error.

See the project in CodeSandbox.

Using the Axios library

Axios is a promise-based HTTP client that connects to an endpoint. In this section, we will use it to fetch post data from an endpoint. Unlike the fetch() method, the response returned from this library contains the JSON format we need.

It also has the advantage of robust error handling, so we don’t need to check and throw an error like we did earlier with the fetch() method.

To use Axios, we must install it:

npm install axios

Once we import it in our app, like so:

import axios from "axios"

We can then use it to perform a GET request:

const response = await axios.get(
  `https://jsonplaceholder.typicode.com/posts?_limit=10`
);

console.log(response)

The library returns an object containing the data we need.

Library Returns Object Data, Showing Nine Objects In An Array

We can access the data from the data property with response.data. In the end, our code looks like this:

useEffect(() => {
  const getData = async () => {
    try {
      const response = await axios.get(
        `https://jsonplaceholder.typicode.com/posts?_limit=10`
      );
      setData(response.data);
      setError(null);
    } catch (err) {
      setError(err.message);
      setData(null);
    } finally {
      setLoading(false);
    }
  };
  getData();
}, []);

This is straightforward and more concise compared to the fetch() method, and you can see it working in the CodeSandbox project.

Before we move to the next method, let’s quickly take a look at fetching data from the GraphQL API endpoint. Remember, we’ve been working with a REST API up to this point.

Fetching data from a GraphQL API endpoint

This approach is similar to the REST API, except that for a GraphQL API, we perform a POST request to the GraphQL server.

In the POST query, we provide the exact data we need and expect a JSON object as the response. This approach solves the problem of over-fetching associated with the REST API.

Note that there are libraries made specifically to connect with the GraphQL API and fetched data, like the Apollo Client; here we will keep things simple and use Axios to fetch data.

In this section, we will fetch mission data from the SpaceX GraphQL server by looking for mission_name.

Fetching Data From SpaceX GraphQL Server, Showcasing Launch Data, Specifically mission_name

We can find the name under the launchesPast in the GraphiQL playground.

Pressing The GraphiQL Play Button In The Top Middle Of The UI, Red Arrow Pointing To It

We get the returned data once we press the Play button, and now we can use the query in our code like so:

const queriedData = ` 
{
  launchesPast(limit: 8) {
    id
    mission_name
  }
}
`;

Next, let’s make a POST request to the server and pass the query inside the request body:

const response = await axios.post(`https://api.spacex.land/graphql/`, {
  query: queriedData
}); // wait until the promise resolves
console.log(response);

If we check the response, we will get the following:

Receiving Data From SpaceX GraphQL, Showing Object Array

Notice where the actual data is located: response.data.data.launchesPast. Now, we can update the state and render the data in the frontend:

// ...
const response = await axios.post(`https://api.spacex.land/graphql/`, {
  query: queriedData
});

setData(response.data.data.launchesPast);
// ...

Rendering The SpaceX Data In The App's Frontend, Showing The List Of Mission Names

See the project on CodeSandbox.

Using the useFetch custom Hook from react-fetch-hook

Up to this point, we’ve covered most of what we need to fetch data from an API. However, we can go a step further by simplifying data fetching using the useFetch Hook from the react-fetch-hook library.

This is a custom Hook that allows us to reuse the fetching logic in the same or different components of our app.

To use the library, we must first install it:

npm i react-fetch-hook

Next, we can import it in our component like so:

import useFetch from "react-fetch-hook";

Then, call useFetch while passing the endpoint URL and destructure the state (isLoading, data, error) from the object, which we can then use in our render:

export default function App() {
  const { isLoading, data, error } = useFetch(
    "https://jsonplaceholder.typicode.com/posts?_limit=10"
  );
  return (
    // ...
  );
}

It doesn’t get simpler. See the project on CodeSandbox.

If you’re wondering how to build a custom Hook that you can reuse just like the above, we’ve provided one in this CodeSandbox. The code is similar to what we’ve been writing so far. So, you are good to go.

Using the React Query library

With the React Query library, we can achieve a lot more than just fetching data. It provides support for caching and refetching, which impacts the overall user experience by preventing irregularities and ensuring our app feels faster.

Like the previous method, React Query provides a custom Hook that we can reuse throughout our app to fetch data. To use the library, let’s install it:

npm i react-query

Next, we must wrap our parent component with the QueryClientProvider imported from react-query and pass the client instance to it:

import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();


ReactDOM.render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>,
  // ...
);

Next, we must fetch data by calling the useQuery Hook from react-query and pass along a unique query key and function that the query uses to fetch data:

import axios from "axios";
import { useQuery } from "react-query";

export default function App() {
  const { isLoading, error, data } = useQuery("posts", () =>
    axios("https://jsonplaceholder.typicode.com/posts?_limit=10")
  );
  console.log(data)
  return (
    // ...
  );
}

Here, we used axios in the fetching function but we can also use the fetch() method.

The object returned by useQuery is destructured, thus we have the information we need in our render.

Notice we’ve logged the data inside the code to see where the actual data is. If we take a look at the console, the actual data is located in the data property (data.data).

Data Located In The Data Property

We can then use the actual data to render the front end. See the project in CodeSandbox.

Conclusion

This guide covers almost everything we need to know about modern data fetching techniques. We learned to fetch data not only from a REST API but also from the GraphQL API.

In addition, we learned how to manage different states like the loading and error states. Now, you should feel more confident fetching data into your React app.

If you find this guide interesting, endeavor to share it around the web. And if you have questions and/or contributions, I’m in the comment section.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

5 Replies to “Modern API data-fetching methods in React”

    1. Hello Tribixbite,

      Thanks for reading through. This is not a list or comparison of the data fetching methods. In this guide, we did not only mention modern ways, but we also showcase how to apply these methods in our application. RTK query is good but it’s included within the installation of the core redux toolkit package and requires a separate article. While the knowledge of redux is not required, it is wise to familiarize yourself with redux. This, I thought would be another layer of complexity for beginners. Moreover, its functionality is similar to React Query, and also takes inspiration from tools like React Query. Anyway, we have an article that covers what it is and how to use it here, https://blog.logrocket.com/rtk-query-future-data-fetching-caching-redux/

      Thank you.

      1. If I can add my 2 cents … I completely agree with what @Ibaslogic has written. At work we use RTK Query. Saying the learning curve is steep doesn’t even come close to accurately describing it. I do understand its advantages but there is a development cost to adopt it.

        I’ve never used React Query but it’s not the silver bullet solution either: https://medium.com/duda/what-i-learned-from-react-query-and-why-i-will-not-use-it-in-my-next-project-a459f3e91887

        And don’t even get me started about React Hook Form (RHK). Though different subject matter, I’ve carefully observed experienced developers, who were strong advocates and supposed experts with RHK take 100% – 400% longer to implement relatively simple forms in React.

        New technologies are absolutely great but developers often have a difficult time keeping their egos and reputations out of the equation when defending technologies that they like to use. For smaller startups, who don’t have unlimited budgets, adopting such “new shiny objects” can potentially destroy a team and the business.

Leave a Reply