Retrieving data from an API is an extremely common task for a developer. Because this is such a frequent operation to complete, it's beneficial to abstract the internals and boilerplate of this functionality into a reusable hook.
In this tutorial, you'll learn how to create a hook, useFetch
, that can help with grabbing data using the native Fetch API. For more information on what custom hooks are, check out my first article on creating a useMediaQuery hook or read the official React docs.
Fetch API Overview
The Fetch API is a native web API that provides an interface for asynchronously fetching resources. There is a fetch() method that allows you to send a network request and get information from the server. The fetch()
method requires one mandatory argument which represents the URL of the resource you would like to obtain data from. It returns a Promise that resolves to the Response of that request. Additionally, you can pass in a second argument, options
, that contains an object with custom settings that you may want to apply to the request.
This is a relatively high-level overview of the Fetch API, so if you'd like to take a deeper dive, make sure to check out the docs on MDN.
Creating the useFetch Hook
🔖 I've created a Github Gist that contains the code for the hook I am about to show you how to implement, so feel free to check that out if you want to reference it later.
To understand the internals of the hook, let's first take a look at the finished code and then break it down.
import { useEffect, useState } from "react";
const useFetch = (initialUrl, options) => {
const [url, setUrl] = useState(initialUrl || "")
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if(!url) return
const fetchData = async () => {
setLoading(true)
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error status: ${response.status}`);
}
const json = await response.json()
setData(json)
setLoading(false)
setError(null)
} catch (error) {
//This catches the error if either of the promises fails or the manual error is thrown
setLoading(false)
setError(error.message)
}
}
fetchData()
}, [url, options]);
return [{ data, loading, error }, setUrl];
}
export default useFetch;
Breaking it Down
- Define a
useFetch
function that accepts two optional parameters:initialUrl
- This defines the URL of the resource that you wish to fetch. This is required if you wish to make an API call when the component is first rendered.options
- An object containing any custom settings that you want to apply to the request. (Check out the parameters section within the MDN docs to see an exhaustive list of available fields that can be passed into this object)
- Inside the function, define four state variables that will house the values of the different states that we need to keep track of when making asynchronous calls.
url
- The URL of the resource that we want to make the fetch call on.data
- The data structure containing the result of the fetch call after it completes.loading
- Indicates whether the async action is in progress or not.error
- If the fetch operation produces an error, this will contain the error message associated with the failed operation.
- Create a
useEffect
hook that will handle all the side effects when theurl
oroptions
arguments change. - If a URL has NOT been provided, then it returns early and no fetch call is instantiated. If a URL has been provided, the
fetchData()
function is invoked, and goes through the following process:- The
loading
state is set totrue
to indicate that a request is in flight. - Since this code leverages async/await, the code is wrapped in a try...catch block to allow for proper error handling.
- The fetch call is initiated with
const response = await fetch(url, options)
. - If the response object doesn't return back with the ok property as
true
then we manually throw an error which will trigger thecatch
block.Read more about why you have to check for
Aresponse.ok
fetch()
promise only rejects when a network error is encountered (which is usually when there's a permissions issue or similar). Afetch()
promise does not reject on HTTP errors (404, etc.). Instead, a separate conditional must check the Response.ok and/or Response.status properties.
- If the request was successful, then initiate the call to get the JSON data with
const json = await response.json()
.- If the Promise from response.json() is resolved successfully, then the
data
,loading
, anderror
states are updated accordingly to reflect this output.
- If the Promise from response.json() is resolved successfully, then the
- The
- The hook returns an array with two fields. The first index includes an object containing the
data
,loading
, anderror
fields. The second index includes thesetUrl
function which provides the ability to dynamically set the URL. (More on why the setter function is being passed back in the next section)
Using the Hook
Method #1 - Fetch on Component Render
There are two separate ways that we can leverage our custom useFetch
hook. The first method involves making an API call when the component is first rendered. This involves passing in the URL to the useFetch
hook when the hook is first instantiated.
ℹ️ Background Info on the Code
All the code snippets in this section are taken from the doggy-directory repository that houses the sample project I used in my "How To Test a React App with Jest and React Testing Library" article I wrote for DigitalOcean. This project leverages the Dog API to build a search and display system for a collection of dog images based on a specific breed.import React, { useState } from "react";
import useFetch from "./hooks/useFetch";
function App() {
const [selectedBreed, setSelectedBreed] = useState("");
const [{ data: breeds, loading: loadingBreeds }] = useFetch("https://dog.ceo/api/breeds/list/all")
//....Rest of App code
}
- As you can see, the
useFetch
hook is declared at the top of the component and passes in the initial URL to fetch from. Thedata
andloading
fields are being destructured out of the array returned back from the hook. In addition, object destructuring and renaming are being used to create thebreeds
andloadingBreeds
to provide more explicit state names.
Method #2 - Fetch at a later time
The second method involves fetching in response to events outside of the component render. This approach involves using the setter function to invoke the fetch call in a more dynamic fashion.
This implementation is emulated after Apollo Client's -
useLazyQuery
hook.
In the previous section, we saw that the hook returns back a setUrl
setter function. Because we have to adhere to the rules of hooks, we cannot call hooks inside nested functions so the setter function provides a way to make the fetch process a bit more dynamic.
import React, { useState } from "react";
import useFetch from "./hooks/useFetch";
function App() {
const [selectedBreed, setSelectedBreed] = useState("");
const [{ data: breeds, loading: loadingBreeds }] = useFetch("https://dog.ceo/api/breeds/list/all")
const [{ data: dogData, loading: loadingImages }, fetchImages] = useFetch()
const searchByBreed = () => {
fetchImages(`https://dog.ceo/api/breed/${selectedBreed}/images`)
};
//....
<button
type="button"
disabled={!selectedBreed}
onClick={searchByBreed}
>
Search
</button>
//....Rest of App code
- In this code block, we have added
const [{ data: dogData, loading: loadingImages }, fetchImages] = useFetch()
which destructures out the object housing the state variables and the setter function which we have relabeled asfetchImages
. - Notice how we are NOT passing in a URL when declaring the
useFetch
hook as we did in the first example. This is intentional as the URL is being set in thesearchByBreed
click handler with thefetchImages()
setter function. - Structuring the
useFetch
hook in this way makes it so we can run a query in response to different events, like the click of a button in this example, while adhering to the rules of hooks.
El Fin 👋🏽
Although this useFetch
implementation is not the most robust solution out in the wild, this is a great option for projects where you want to get up and running quickly without a third-party library. Plus, it solves most use cases when grabbing data for side projects.
For alternative data fetching solutions, check out popular libraries like SWR, React Query, and, if using GraphQL, Apollo Client's useQuery
hook.
If you enjoy what you read, feel free to like this article or subscribe to my newsletter, where I write about programming and productivity tips.
As always, thank you for reading, and happy coding!