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:
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:
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.
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.
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
.
We can find the name under the launchesPast
in the GraphiQL playground.
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:
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); // ...
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
).
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.
Cut through the noise of traditional React error reporting with LogRocket
LogRocket is a React analytics solution that shields you from the hundreds of false-positive errors alerts to just a few truly important items. LogRocket tells you the most impactful bugs and UX issues actually impacting users in your React applications.

Focus on the React bugs that matter — try LogRocket today.
I love React Query
I am really confused why you don’t mention rtk query
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.
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.
Awesome material
So what should I use and in which cases?
Thanks, this has been an amazing article, really professional and well written!
btw, it took me a while to find this in the official documentation: I’d like to point out an important detail: passing a second arugment “[]” to the useEffect(…) function is VERY IMPORTANT. This non-obvious trick makes this specific effect function run only once. Otherwise the function passed to useEffect will be executed every time the component’s state is modified. So the fetch code runs into an infinite loop, if this is omitted.
Hi Steinbach,
Thank you for your input. It was stated in this tutorial that the effect with ‘[]’ will run and fetch data on a component mount, that is, on the first render. That is basic React fundamentals.
To expand on how the useEffect(…) works:
Naturally, the effect runs after every completed render. That is, on the first component render and after it detects a state or props changes in the dependency array. If we leave the dependency array empty, React will skip any form of re-rendering and only execute the effects once.
useEffect(() => {
// effect here
}, []);
However, we must only leave the array empty if the effect does not use a value from the rendered scope. In other words, the effect doesn’t use values from inside the component. If the effect use component values like props, state, or event functions, we must include them as dependencies in the array.
We mustn’t be tempted to empty the array while the effect uses the component’s values. Going against this rule gives React a false impression that the effect doesn’t depend on any component’s value. Whereas, it does! We must leave React to decide when to re-run a component; ours is to pass every necessary hint through the dependencies array.
Thank you.
And if component unmounts while fetch promise is still pending…?