3 min read

Tutorial: Build a custom useFetch hook

To get started with our custom hook, let's first take a look at how the PokemonList component would look like if we didn't have a useFetch hook:

import { useEffect, useState } from "react";

type Pokemon = {
  name: string;
};

const PokemonList = () => {
  const [pokemons, setPokemons] = useState<Pokemon[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string>();

  useEffect(() => {
    const fetchPokemonData = async () => {
      const params = new URLSearchParams({ limit: "10", offset: "0" });
      try {
        setIsLoading(true);
        const response = await fetch(
          `https://pokeapi.co/api/v2/pokemon?${params}`
        );
        const data = await response.json();

        setPokemons(data.results);
        setIsLoading(false);
      } catch (e) {
        const errorMessage = (e as { message: string }).message;
        setError(errorMessage);
        setIsLoading(false);
      }
    };

    fetchPokemonData();
  }, []);

  if (isLoading) {
    return <p>Loading ...</p>;
  }

  if (error) {
    return <p>{error}</p>;
  }

  return (
    <ol>
      {pokemons.map((pokemon) => (
        <li key={pokemon.name}>{pokemon.name}</li>
      ))}
    </ol>
  );
};

export default PokemonList;

Basically, we would just use three useState hooks, one each for the data, isLoading and the error.

Creating the custom hook

To create our custom hook, we just need to move this logic to the hook function:

import { useEffect, useState } from "react";

const useFetch = (url, options) => {
  const [data, setData] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string>();

  useEffect(() => {
    async function loadData() {
      try {
        setIsLoading(true);
        const response = await fetch(url, options);
        const data = await response.json();
        setData(data);
        setIsLoading(false);
      } catch (e) {
        setError((e as { message: string }).message);
        setIsLoading(false);
      }
    }

    loadData();
  }, [url, options]);

  return { data, isLoading, error };
};

export { useFetch };

This works, but we get a lot of squiggly lines as we didn't add types yet. Let's do that next.

Typing the custom hook

It's easy to type the url and error, as they are both strings.
For the options, we can type it as an object with string keys and string values: { [key: string]: string }. We can also pass a default value of {}.

const useFetch = (url: string, options: { [key: string]: string } = {}): { data: unknown; isLoading: boolean; error: string } => {
  const [data, setData] = useState<unknown>();
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [error, setError] = useState<string>();

	...
  
  return { data, isLoading, error };
};

But what about data? We don't know ahead of time what structure the server response will have. We need a way to allow the users to pass the type of the response - which we can do using generics!

Since we are using an arrow function to define the hook, to pass the generic type we can append it to the beginning of the function:

import { useEffect, useState } from "react";

const useFetch = <T>(
  url: string,
  options: { [key: string]: string } = {}
): { data: T | undefined; isLoading: boolean; error?: string } => {
  const [data, setData] = useState<T>();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string>();

  useEffect(() => {
    async function loadData() {
      try {
        setIsLoading(true);
        const response = await fetch(url, options);
        const data = await response.json();
        setData(data);        
        setIsLoading(false);
      } catch (e) {
        setError((e as { message: string }).message);
        setIsLoading(false);
      }
    }

    loadData();
  }, [url, options]);
  return { data, isLoading, error };
};

export { useFetch };

Let's now run the tests and see where we stand:

npm run test

The tests for the PokemonList component now pass, but we seem to have some failures on the useFetch hook test:

Looks like our code fails when the sever returns 401 or 500 error codes? Why is that?

Handling HTTP error codes

Our hook uses fetch for retrieving the data and fetch does not throw when the server response has an error code! It simply sets the ok property of the response to false.
Thus, we need to manually check for it and return an error if response is not ok:

Here is the updated loadData function:

async function loadData() {
  try {
	setIsLoading(true);
	const response = await fetch(url, options);
	const data = await response.json();
	if (response.ok) {
	  setData(data);
	} else {
	  setError(data.error);
	}
	setIsLoading(false);
  } catch (e) {
	setError((e as { message: string }).message);
	setIsLoading(false);
  }
}

And with this change, our tests now pass! 💪

Here is the final completed hook:

import { useEffect, useState } from "react";

const useFetch = <T>(
  url: string,
  options: { [key: string]: string } = {}
): { data: T | undefined; isLoading: boolean; error?: string } => {
  const [data, setData] = useState<T>();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string>();

  useEffect(() => {
    async function loadData() {
      try {
        setIsLoading(true);
        const response = await fetch(url, options);
        const data = await response.json();
        if (response.ok) {
          setData(data);
        } else {
          setError(data.error);
        }
        setIsLoading(false);
      } catch (e) {
        setError((e as { message: string }).message);
        setIsLoading(false);
      }
    }

    loadData();
  }, [url, options]);
  return { data, isLoading, error };
};

export { useFetch };

You can just paste this into the starter-repo code and your will see all tests turn green ✅.

Get the React Practice Calendar!

28 days of focused practice of increasing difficulty, going through everything from Fundamentals, Data fetching, Forms and using Intervals in React.

You will also get notified whenever a new challenge is published.