Custom Hooks in React: useFetch

Custom Hooks in React: useFetch

ยท

7 min read

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

  1. 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)
  2. 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.
  3. Create a useEffect hook that will handle all the side effects when the url or options arguments change.
  4. 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 to true 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 the catch block.
      • Read more about why you have to check for response.ok A fetch() promise only rejects when a network error is encountered (which is usually when there's a permissions issue or similar). A fetch() 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, and error states are updated accordingly to reflect this output.
  5. The hook returns an array with two fields. The first index includes an object containing the data, loading, and error fields. The second index includes the setUrl 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. The data and loading fields are being destructured out of the array returned back from the hook. In addition, object destructuring and renaming are being used to create the breeds and loadingBreeds 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 as fetchImages.
  • 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 the searchByBreed click handler with the fetchImages() 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!