How to fetch data from a back-end in React
Today, we're going to talk about fetching data in React. One of the most common things you'll see in a React application is interacting with and fetching data from a back-end. In this article, we'll demonstrate how to fetch data from an external API using the Fetch API.
Table of contents
What is Fetch?
The Fetch API provides an interface for fetching resources (including across the network). It will seem familiar to anyone who has used XMLHttpRequest, but the new API provides a more powerful and flexible feature set.
Put simply, it's a newer, more flexible interface to make HTTP requests without the need for the ancient XMLHttpRequest API (browser compatibility permitting).
Tutorial
Here's a preview of what we're going to build today (the data is fetched from a mock API).
State
Let's see how Fetch works in action and start by adding state to store our data that we are about to fetch. We are going to be interacting with a mock endpoint which returns tasks to complete (like a to do list), so let's call it tasks
.
1const App = () => {2 const [tasks, setTasks] = useState([]);34 return <h1>Fetch API</h1>;5};
Note: Remember to import
useState
if you haven't already.
Effects
Now, we need to fetch the tasks. Let's create an effect in our App
component with no dependencies:
1const App = () => {2 const [tasks, setTasks] = useState([]);34 useEffect(() => {5 // This code will run when the component mounts.6 }, []);78 return <h1>Fetch API</h1>;9};
Note: Remember to import
useEffect
if you haven't already.
Now, we're going to be using the async
await
syntax in this article, but you can use the .then
syntax if you prefer. As fetch
is asynchronous and returns a promise, we need to await
it. Effect callbacks cannot be async
, so we cannot do this:
1useEffect(async () => {2 // We cannot do this.3}, []);
Instead, my preferred method is to declare an asynchronous function within our effect, and invoke it below. Let's see what that looks like:
1useEffect(() => {2 const fetchTasks = async () => {3 // ...4 };56 fetchTasks();7}, []);
Requests
Now, let's write our request and then step through it afterwards.
1useEffect(() => {2 const fetchTasks = async () => {3 const response = await fetch("https://jsonplaceholder.typicode.com/todos");45 const tasks = await response.json();67 setTasks(tasks);8 };910 fetchTasks();11}, []);
Let's step through this code:
- First, we store the
fetch
response in a variable namedresponse
- Secondly, we parse the response as JSON by calling
.json()
on theresponse
- Finally, we store these tasks in state.
Rendering
Great, now let's render our tasks.
The shape of the data returned by the API looks like this:
1{2 id: 1,3 title: "delectus aut autem",4}
Note: This example only shows the data we need for this article.
Let's map
over these tasks and render them:
1const App = () => {2 const [tasks, setTasks] = useState([]);34 useEffect(() => {5 const fetchTasks = async () => {6 const response = await fetch("https://jsonplaceholder.typicode.com/todos");78 const tasks = await response.json();910 setTasks(tasks);11 };1213 fetchTasks();14 }, []);1516 return (17 <section>18 <h1>Fetch API</h1>1920 <ul>21 {tasks.map((task) => (22 <li key={task.id}>{task.title}</li>23 ))}24 </ul>25 </section>26 );27};
Throwing errors
Fantastic, but what happens if our request isn't successful? Let's simulate this by making a request to an endpoint that doesn't exist.
1const response = await fetch("https://jsonplaceholder.typicode.com/nonexistantendpoint");
Oops, we get an error. These types of operations should be wrapped in a try catch
block to handle such cases.
Let's see what that looks like:
1useEffect(() => {2 const fetchTasks = async () => {3 try {4 const response = await fetch("https://jsonplaceholder.typicode.com/nonexistantendpoint");56 const tasks = await response.json();78 setTasks(tasks);9 } catch (error) {10 console.error("Something went wrong...");11 }12 };1314 fetchTasks();15}, []);
Now, you may have noticed that our catch
block is never executed, and our error message is never written to the console. This is because we need to manually throw an error if our request isn't ok
(more on this next).
1useEffect(() => {2 const fetchTasks = async () => {3 try {4 const response = await fetch("https://jsonplaceholder.typicode.com/nonexistantendpoint");56 const tasks = await response.json();78 if (!response.ok) {9 throw new Error();10 }1112 setTasks(tasks);13 } catch (error) {14 console.error("Something went wrong...");15 }16 };1718 fetchTasks();19}, []);
Now, if our request doesn't respond with ok
, then we know our request wasn't successful. This is a boolean which allows us to easily check if our request was successful by checking a range of HTTP status codes (2**).
The
ok
read-only property of theResponse
interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.
Additional state
You may have noticed that our application has no loading or error states, so let's add those now:
1const [loading, setLoading] = useState(false);2const [error, setError] = useState(false);3const [tasks, setTasks] = useState([]);
And let's set our loading
state to true when we start our request, and error
to true if we catch an error.
1useEffect(() => {2 const fetchTasks = async () => {3 try {4 setLoading(true);56 const response = await fetch("https://jsonplaceholder.typicode.com/nonexistantendpoint");78 const tasks = await response.json();910 if (!response.ok) {11 throw new Error();12 }1314 setTasks(tasks);15 } catch (error) {16 setError(true);17 }18 };1920 fetchTasks();21}, []);
A common pattern is to set loading
to false after we've set our tasks, and again once we've set our error. We can improve on this by adding a finally
block which will run once everything else above it has executed.
1useEffect(() => {2 const fetchTasks = async () => {3 try {4 setLoading(true);56 const response = await fetch("https://jsonplaceholder.typicode.com/nonexistantendpoint");78 const tasks = await response.json();910 if (!response.ok) {11 throw new Error();12 }1314 setTasks(tasks);15 } catch (error) {16 setError(true);17 } finally {18 setLoading(false);19 }20 };2122 fetchTasks();23}, []);
Loading and error handling
Awesome. Now, let's add our actual endpoint back in and handle these states with early returns. Let's see what that looks like:
1const App = () => {2 const [loading, setLoading] = useState(false);3 const [error, setError] = useState(false);4 const [tasks, setTasks] = useState([]);56 useEffect(() => {7 const fetchTasks = async () => {8 try {9 setLoading(true);1011 const response = await fetch("https://jsonplaceholder.typicode.com/todos");1213 const tasks = await response.json();1415 if (!response.ok) {16 throw new Error();17 }1819 setTasks(tasks);20 } catch (error) {21 setError(true);22 } finally {23 setLoading(false);24 }25 };2627 fetchTasks();28 }, []);2930 if (loading) {31 return <p>Loading...</p>;32 }3334 if (error) {35 return <p>Error.</p>;36 }3738 return (39 <section>40 <h1>Fetch API</h1>4142 <ul>43 {tasks.map((task) => (44 <li key={task.id}>{task.title}</li>45 ))}46 </ul>47 </section>48 );49};
Great, now you'll notice a quick loading state whilst we make our request which then disappears once the tasks have been fetched. Also, if you simulate an endpoint which doesn't exist again, our error message shows.
Abstracting logic out to a custom hook
You may have noticed that we are beginning to pollute our component with a lot of code which could be kept elsewhere.
This is an ideal use case for writing our own custom hook. Custom hooks are often easy to write and are often an ideal solution to reusing and sharing logic between components.
Writing the hook
Let's create a new folder within src
called hooks
and create a file called useRequest.js
.
Your hook should look like this:
1import { useState, useEffect } from "react";23const useRequest = () => {4 // ...5};67export default useRequest;
Now, let's port over our state and logic in to our new hook.
1const useRequest = () => {2 const [loading, setLoading] = useState(false);3 const [error, setError] = useState(false);4 const [tasks, setTasks] = useState([]);56 useEffect(() => {7 const fetchTasks = async () => {8 try {9 setLoading(true);1011 const response = await fetch("https://jsonplaceholder.typicode.com/todos");1213 const tasks = await response.json();1415 if (!response.ok) {16 throw new Error();17 }1819 setTasks(tasks);20 } catch (error) {21 setError(true);22 } finally {23 setLoading(false);24 }25 };2627 fetchTasks();28 }, []);29};
As making GET requests to an endpoint is very common, let's make this hook generic and reusable.
We can start by refactoring a few things to be more generic:
1const useRequest = () => {2 const [loading, setLoading] = useState(false);3 const [error, setError] = useState(false);4 const [data, setData] = useState([]);56 useEffect(() => {7 const fetchData = async () => {8 try {9 setLoading(true);1011 const response = await fetch("https://jsonplaceholder.typicode.com/todos");1213 const data = await response.json();1415 if (!response.ok) {16 throw new Error();17 }1819 setData(data);20 } catch (error) {21 setError(true);22 } finally {23 setLoading(false);24 }25 };2627 fetchData();28 }, []);29};
All we need to do now is make the endpoint (the URL) a parameter of our hook so we can pass in any endpoint and reuse our request hook throughout our application.
We will also need to make this a dependency of our effect, as we want it to re-run if we change the URL.
1const useRequest = (url) => {2 const [loading, setLoading] = useState(false);3 const [error, setError] = useState(false);4 const [data, setData] = useState([]);56 useEffect(() => {7 const fetchData = async () => {8 try {9 setLoading(true);1011 const response = await fetch(url);1213 const data = await response.json();1415 if (!response.ok) {16 throw new Error();17 }1819 setData(data);20 } catch (error) {21 setError(true);22 } finally {23 setLoading(false);24 }25 };2627 fetchData();28 }, [url]);29};
Now, we need to return our loading
, error
and data
states from our hook so we can consume them from within our component.
Your hook should now look like this:
1import { useState, useEffect } from "react";23const useRequest = (url) => {4 const [loading, setLoading] = useState(false);5 const [error, setError] = useState(false);6 const [data, setData] = useState([]);78 useEffect(() => {9 const fetchData = async () => {10 try {11 setLoading(true);1213 const response = await fetch(url);1415 const data = await response.json();1617 if (!response.ok) {18 throw new Error();19 }2021 setData(data);22 } catch (error) {23 setError(true);24 } finally {25 setLoading(false);26 }27 };2829 fetchData();30 }, [url]);3132 return { loading, error, data };33};3435export default useRequest;
Using the hook
Now, in our App
component, let's import our hook and use it. If you're used to using hooks in React, this should look familiar.
We'll pass in our endpoint to fetch from, and receive our loading, error, and data states. Here we're renaming data
to tasks
in our destructuring assignment to keep the naming consistent.
1const App = () => {2 const { loading, error, data: tasks } = useRequest("https://jsonplaceholder.typicode.com/todos");34 if (loading) {5 return <p>Loading...</p>;6 }78 if (error) {9 return <p>Error.</p>;10 }1112 return (13 <section>14 <h1>Fetch API</h1>1516 <ul>17 {tasks.map((task) => (18 <li key={task.id}>{task.title}</li>19 ))}20 </ul>21 </section>22 );23};
Note: Remember to import the
useRequest
hook if you haven't already.
That's it. Now our App
component is much cleaner.
Example
Here's what we've built:
React Query
If you find you need a more advanced feature set including things such as caching and optimistic updates, I'd suggest taking a look at React Query.
Fetch, cache and update data in your React and React Native applications all without touching any "global state".
React Query is extremely powerful and can help to solve many problems when making requests in React. We've only covered making simple GET requests, but it can help you with much more than that.
Conclusion
There are many different ways to approach this, and ways to improve the code in this article. I purposefully didn't cover every edge case or make every abstraction possible to make it easier to follow.
Hopefully you learnt a thing or two about fetching data in React, and I hope you found this article insightful.