DEV Community

Cover image for Custom React useFetch() hook for data fetching with revalidation
damilola jerugba for Brimble

Posted on • Originally published at damiisdandy.com

Custom React useFetch() hook for data fetching with revalidation

This guide is to show you how to create a simple react hook for data fetching (with revalidation).

๐Ÿคจ Why this hook?

When fetching data for your react applications, you'd usually use both useState and useEffect, with values like loading, data and error e.g This example, this hook is to help abstract that functionality into one simple hook that can be used anywhere and multiple times.

๐Ÿ•บ Setting up the project

We would be using the create-react-app boiler template for typescript and the only external library we would be using is axios for data fetching.

Open up your terminal and type in the following commands.

yarn create react-app use-fetch --template typescript
# for npm
npx create-react-app use-fetch --template typescript
Enter fullscreen mode Exit fullscreen mode

Change into the directory and install axios

cd use-fetch
yarn add axios
# for npm
npm install axios
Enter fullscreen mode Exit fullscreen mode

Within the src directory delete the following file (because they aren't needed)

  • App.css
  • App.test.tsx

๐ŸŽฃ Custom useFetch hook

Within the src directory create another directory called hooks, this is where our hook will reside.

cd src
mkdir hooks
Enter fullscreen mode Exit fullscreen mode

Your file structure should look something like this.

file structure

Within the hooks directory create a file called useFetch.tsx.

Type in the following inside the useFetch file.

import { useState, useEffect, useCallback } from "react";
import axios from "axios";

interface UseFetchProps {
  url: string;
}

const useFetch = ({ url }: UseFetchProps) => {
  const [data, setData] = useState<any>();
  const [error, setError] = useState(false);

  // function to fetch data
  const fetch = useCallback(async () => {
    setError(false);
    try {
      const fetchedData = await axios.get(url);
      setData(fetchedData.data);
    } catch {
      setError(true);
    }
  }, [url]);

  useEffect(() => {
    // on first load fetch data
    fetch();
  }, [fetch]);

  return {
    data,
    error,
    revalidate: fetch,
  };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

The hook takes in a prop url, which is the API url at which we want to fetch data from. It has two states data and error which are used to store data gotten from the API and check for errors respectively.

We created a separate function for fetching the data called fetch and wrapped it within a useCallback hook, Visit here to see the reason why we used a useCallback hook.

Then we simply used a useEffect hook to run the fetch function as soon as the hook is mounted ๐Ÿ™‚.

The hook returns data, error and revalidate which is the fetch function for when we want to programmatically revalidate the data.

๐Ÿ˜Ž Using the hook

To use the hook we simply just import it and extract its values.
Within the App.tsx

import useFetch from "./hooks/useFetch";
import logo from "./logo.svg";

function App() {
  const { error, data, revalidate } = useFetch({
    url: "https://random-data-api.com/api/users/random_user?size=5",
  });

  if (!data) {
    return <h2>Loading...</h2>;
  }

  if (error) {
    return <h2>Error fetching users</h2>;
  }

  return (
    <div className="App">
      <img src={logo} alt="react logo" />
      <h1 className="title">useFetch()</h1>
      <button onClick={revalidate}>revalidate</button>
      <div className="items">
        {data.map((el: any) => (
          <div className="item" key={el.uid}>
            <img
              src={`https://avatars.dicebear.com/api/big-smile/${el.first_name}.svg`}
              alt={`${el.username} profile`}
              className="item__img"
            />
            <div className="item__info">
              <p className="name">
                {el.first_name} {el.last_name}{" "}
                <span className="username">(@{el.username})</span>
              </p>
              <p className="job">{el.employment.title}</p>
              <p
                className={`status ${
                  el.subscription.status.toLowerCase() === "active"
                    ? "success"
                    : el.subscription.status.toLowerCase() === "blocked"
                    ? "danger"
                    : "warn"
                }`}
              >
                {el.subscription.status}
              </p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

โฐ Adding Interval revalidation

You might need to fetch data from your API every 5 seconds for revalidation (ensuring your data is up-to-date).

We need to add some modifications to our useFetch hook. Lets and more props.

interface UseFetchProps {
  url: string;
  revalidate?: boolean;
  interval?: number;
}
Enter fullscreen mode Exit fullscreen mode

revalidate will be a boolean to check if we want to implement interval revalidation or not, interval will be the time taken between every revalidation (in seconds).

...
const useFetch = ({ url, revalidate, interval }: UseFetchProps) => {
...
Enter fullscreen mode Exit fullscreen mode

We'll create a state called revalidateKey that we will change on every interval which will be added to our useEffect dependency array. Adding this to our dependency array will ensure that the function within our useEffect will run everytime the revalidateKey changes.

To change the revalidateKey, we will create a new useEffect that has a setInterval.

...
const [revalidateKey, setRevalidateKey] = useState("");
...
useEffect(() => {
    const revalidateInterval = setInterval(() => {
      if (revalidate) {
        setRevalidateKey(Math.random().toString());
      }
      // if no interval is given, use 3 seconds
    }, (interval ? interval : 3) * 1000);
    return () => clearInterval(revalidateInterval);
  }, [interval, revalidate]);
Enter fullscreen mode Exit fullscreen mode

Our useFetch hook should then look something like this.

const useFetch = ({ url, revalidate, interval }: UseFetchProps) => {
  const [revalidateKey, setRevalidateKey] = useState("");
  const [data, setData] = useState<any>();
  const [error, setError] = useState(false);

  // function to fetch data
  const fetch = useCallback(async () => {
    setError(false);
    try {
      const fetchedData = await axios.get(url);
      setData(fetchedData.data);
    } catch {
      setError(true);
    }
  }, [url]);

  useEffect(() => {
    const revalidateInterval = setInterval(() => {
      if (revalidate) {
        setRevalidateKey(Math.random().toString());
      }
      // if no interval is given, use 3 seconds
    }, (interval ? interval : 3) * 1000);
    return () => clearInterval(revalidateInterval);
  }, [interval, revalidate]);

  useEffect(() => {
    // on first load fetch data and when revalidateKey changes
    fetch();
  }, [fetch, revalidateKey]);

  return {
    data,
    error,
    revalidate: fetch,
  };
};
Enter fullscreen mode Exit fullscreen mode

Using the useFetch hook โœจ

const { error, data, revalidate } = useFetch({
    url: "https://random-data-api.com/api/users/random_user?size=5",
    revalidate: false,
    // fetch every 5 seconds
    interval: 5,
  });
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ Graphql support

This hook uses only the GET method, and Graphql uses POST method for data fetching. To make the hook more dynamic you can add more props like isGraphql and query, isGraphql will be a boolean to check if its Graphql or REST so you can have a condition in your hook to use axios.post() instead of axios.get() and query for the graphql query.

Thank you for reading ๐Ÿ™๐Ÿพ, If you have any questions, additions, or subtractions please comment below.

The full source code is linked below ๐Ÿ‘‡๐Ÿ‘‡

GitHub logo brimblehq / use-fetch

data fetching hook with revalidation

Top comments (14)

Collapse
 
zaheeralii profile image
ZaheerAlii

Cool Work, But I'm agree with one of the user that could you add some cancel request.

Collapse
 
chema profile image
Josรฉ Marรญa CL

Nice! Working with graphql I saw a hook called useQuery which returns loading, data, error and refetch.

So, after some attempts to decouple the gql or rest implementation we've created a hook very similar to your useFetch which returns the four variables like the useQuery.

I think that the loading flag could be useful for this hook too. You know, we can infer it if the data has a value but the loading will help you to know if there is a "fetching" task in progress

Collapse
 
damiisdandy profile image
damilola jerugba

Yeah, I kept thinking whether I should add a loading value or not, I initially did but I removed it, You can contribute to the repo.

Collapse
 
activenode profile image
David Lorenz

Isn' that what SWR does? Why not go with Standards?

Collapse
 
damiisdandy profile image
damilola jerugba

Itโ€™s always fun to build and know how things work

Collapse
 
activenode profile image
David Lorenz

I agree. But for production it's mostly better to go with the standards ๐Ÿค—

Collapse
 
pcelac profile image
Aleks

From quick look of example code, it will show loading state for infinite time, if your api call fails.

Collapse
 
damiisdandy profile image
damilola jerugba • Edited

It sets error to true, which displays

if (error) {
    return <h2>Error fetching users</h2>;
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kylesureline profile image
Kyle Scheuerlein

Are you sure? It looks like data would be falsy (if the fetch errors out!) and so your second if block can never run.

Thread Thread
 
damiisdandy profile image
damilola jerugba • Edited

git clone the repo and look for yourself, if this is really an issue please add an issue to the repo or try to contribute to it ๐Ÿ’œ

Collapse
 
harshhhdev profile image
Harsh Singh

Cool work!

Collapse
 
damiisdandy profile image
damilola jerugba

Thank you

Collapse
 
minnyww profile image
apisit Amunayworrabut

Can u add cancel request when components is unmount

Collapse
 
damiisdandy profile image
damilola jerugba

Hmmm, Could you add it as a GitHub issue