DEV Community

Cover image for React Custom Hook - useFetch
Andrew
Andrew

Posted on • Updated on

React Custom Hook - useFetch

I have a YouTube video if you'd rather watch 😎🍿.

Why useFetch?

It's very common to fetch data when the user goes to a certain page. We also use common logic when fetching that data.

There's also a fair amount of boilerplate/logic that crowds our components and it's not very DRY (Don't Repeat Yourself).

These are all good reasons to make a custom hook. We can outsource that boilerplate/logic into one separate file. That file will hold the function (hook) which will return for us what we need to use in our components.

The Old Way

In this example, I'll use the useState hook to keep track of the loading state, any error, and the data. I'll use the useEffect hook to run all of that code. Lastly, I'm using axios to fetch the data, and a cancel token to cancel any unfinished requests that we don't need anymore.

//App.js
import { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';

function App() {
  const [quote, setQuote] = useState(null);
  const [loading, setLoading] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
      setLoading('loading...')
      setQuote(null);
      setError(null);
      const source = axios.CancelToken.source();
      axios.get('https://api.quotable.io/random', { cancelToken: source.token })
      .then(res => {
          setLoading(false);
          setQuote(res.data.content);
      })
      .catch(err => {
          setLoading(false)
          setError('An error occurred. Awkward..')
      })
      return () => {
          source.cancel();
      }
  }, [])

  return (
    <div className="App">
      <button onClick={fetchQuote}>Fetch Quote</button>
      { loading && <p>{loading}</p> }
      { quote && <p>"{quote}"</p> }
      { error && <p>{error}</p> }
    </div>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

That's a lot of code. Let's move most of it.

The New Way

We'll create another file called useFetch.js. You want to start the name of a custom hook with "use" so that React knows to treat it like a hook.

Let's copy over the import statements, all 3 useStates, and the useEffect function.

//useFetch.js
import { useState, useEffect } from 'react';
import axios from 'axios';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
      setLoading('loading...')
      setData(null);
      setError(null);
      const source = axios.CancelToken.source();
      axios.get(url, { cancelToken: source.token })
      .then(res => {
          setLoading(false);
          //checking for multiple responses for more flexibility 
          //with the url we send in.
          res.data.content && setData(res.data.content);
          res.content && setData(res.content);
      })
      .catch(err => {
          setLoading(false)
          setError('An error occurred. Awkward..')
      })
      return () => {
          source.cancel();
      }
  }, [url])

  return { data, loading, error }

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

You may have noticed some changes. First of all, the function (which is our hook), is named useFetch. It receives a parameter which is the url we want to get data from.

We also changed setQuote to setData, making it more versatile. Notice that we also check for multiple responses to make it more flexible as well.

Lastly, our useFetch function (hook) returns our data, loading, and any error.

I put those in an object so we can use object destructuring when accessing those in our component. That way, the order doesn't matter when we destructure them, and we can rename them if we want. I'll show you that next.

Using useFetch in Our Component

So, back in our App component, we'll import our useFetch hook from useFetch.js, and pass in the url we want to fetch data from. We'll use object destructuring to access what we need. Lastly, we'll rename data to quote.

import useFetch from './useFetch';
import './App.css';

function App() {
  const { data: quote, loading, error } = useFetch('https://api.quotable.io/random')

  return (
    <div className="App">
      { loading && <p>{loading}</p> }
      { quote && <p>"{quote}"</p> }
      { error && <p>{error}</p> }
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Muuuuuch cleaner 😎.

Conclusion

Custom hooks are very useful for cleaning up your code. You can use React hooks inside of your custom hooks (they're all functions after all! πŸ‘). You can encapsulate a lot of repetitive logic, then return what you need from the custom hook.

If you like learning about similar topics, feel free to check out my YouTube or Instagram.

Hope this helped somebody and thanks for reading!

-Andrew

Top comments (13)

Collapse
 
helloty profile image
Tyheir Brooks • Edited

Hello, I've created a typescript version of this with the ability to make different type of requests and destroying old requests. Use useFetch for fetching on mount and useMyFetch for triggering requests. Never write another fetch request again :).
PS: dev.to Devs, add an overflow for long code blocks.

import axios, { AxiosError, AxiosResponse } from "axios";
import { useEffect, useReducer, useCallback } from "react";

const useFetch = <T>({ url, method, body, config }: FetchParams) => {
  const [state, dispatch] = useReducer(reducer, {
    data: null,
    isLoading: false,
    error: null,
  });

  function reducer(state: State<T>, action: Action<T>) {
    switch (action.type) {
      case "loading":
        return { ...state, isLoading: true };
      case "success":
        return { data: action.data, isLoading: false, error: null };
      case "error":
        return { data: null, isLoading: false, error: action.error };
      default:
        throw new Error("Unknown action type");
    }
  }
  useEffect(() => {
    let shouldCancel = false;

    const callFetch = async () => {
      dispatch({ type: "loading", error: undefined });

      try {
        const response = await fetch(url, method, body, config);
        if (shouldCancel) return;
        dispatch({ type: "success", data: response.data });
      } catch (error: any) {
        if (shouldCancel) return;
        dispatch({ type: "error", error });
      }

      callFetch();
      return () => (shouldCancel = true);
    };
  }, [url]);

  return { state };
};
export default useFetch;

export const useMyFetch = <T>({ url, method, config }: FetchParams) => {
 // same reducer syntax

  const fetchData = useCallback(
    async (data: any) => {
      try {
        dispatch({ type: "loading", error: undefined });
        const response = await fetch(url, method, data, config);
        dispatch({ type: "success", data: response.data });
        return response.data;
      } catch (error: any) {
        dispatch({ type: "error", error });
        console.error(error);
        throw error;
      }
    },
    [url]
  );

  return { state, fetchData };
};

const fetch = async (
  url: string,
  method: Methods,
  body?: any,
  config?: any
): Promise<AxiosResponse> => {
  console.log({ body });
  switch (method) {
    case "POST":
      return await axios.post(url, body, config);
    case "GET":
      return await axios.get(url, config);
    case "DELETE":
      return await axios.delete(url, config);
    default:
      throw new Error("Unknown request method");
  }
};

type Methods = "POST" | "GET" | "DELETE";

type FetchParams = {
  url: string;
  method: Methods;
  body?: any;
  config?: any;
};

// response.data attribute defined as string until needed otherwise.
type State<T> =
  | { data: null; isLoading: boolean; error: null }
  | { data: null; isLoading: boolean; error: AxiosError }
  | { data: T; isLoading: boolean; error: null };

type Action<T> =
  | { type: "loading"; error: undefined }
  | { type: "success"; data: T }
  | { type: "error"; error: AxiosError };

Enter fullscreen mode Exit fullscreen mode

"Even A Low-Class Warrior Can Surpass An Elite, With Enough Hard Work."

Collapse
 
dhan46code profile image
dhan46code

Awesome this article very clear thank you so much ! i like that

Collapse
 
dastasoft profile image
dastasoft • Edited

Very good article, it's good not to repeat and also isolate the logic of the component.

One tip, for me mixing the load state between a string and a boolean is a bit confusing, I know JS allows you to do this and a non-empty string is truthy but personally I think it's better to keep the same data type throughout the application, especially when you are using it only as a flag not to display content.

The different states you have there are closely related so it might be a good idea to use a useReducer, check this article by Kent C. Dodds I bet it will be useful to improve the hook.

Collapse
 
techcheck profile image
Andrew

Agreed. I used useReducer on my previous article (dev.to/techcheck/react-hooks-usere...). Good call on Kent's article πŸ‘Œ.

Collapse
 
adamjarling profile image
Adam J. Arling • Edited

Great article and very clear. Thanks! I've extended the logic a bit to work with TypeScript and data besides a URL in case anyone else has a use case where a user action triggers a need to re-fetch data.

import { useEffect, useState } from "react";
import { ApiSearchRequest } from "@/types/api/request";
import { ApiSearchResponse } from "@/types/api/response";
import { DC_API_SEARCH_URL } from "@/lib/endpoints";
import { UserFacets } from "@/types/search-context";
import { buildQuery } from "@/lib/queries/builder";

type ApiData = ApiSearchResponse | null;
type ApiError = string | null;
type Response = {
  data: ApiData;
  error: ApiError;
  loading: boolean;
};

const useFetchApiData = (
  searchTerm: string,
  userFacets: UserFacets
): Response => {
  const [data, setData] = useState<ApiData>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<ApiError>(null);

  useEffect(() => {
    setLoading(true);
    setData(null);
    setError(null);

    const body: ApiSearchRequest = buildQuery(searchTerm, userFacets);

    fetch(DC_API_SEARCH_URL, {
      body: JSON.stringify(body),
      headers: {
        "Content-Type": "application/json",
      },
      method: "POST",
    })
      .then((res) => res.json())
      .then((json) => {
        setLoading(false);
        setData(json);
      })
      .catch((err) => {
        setLoading(false);
        setError("Error fetching API data");

        console.error("error fetching API data", err);
      });
  }, [searchTerm, userFacets]);

  return { data, error, loading };
};

export default useFetchApiData;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
fcfidel profile image
Fidel Castro

this is great thank you!

Collapse
 
techcheck profile image
Andrew

Thanks!

Collapse
 
daveteu profile image
Dave

Good write up on the hook. I was looking to create a useFetch hook and came to this, though my intention was completely different.

I was looking to create an useFetch hook that returns a fetch function that handles all the error (displaying them in snackbar), so you will end up handling all the error in one place.

For the same functionality in your write up, with additional feature to memorize and caching the results, may I also recommend useSWR. It's created by the same people behind NextJS and vercel and it's really good.

Collapse
 
techcheck profile image
Andrew

Wow, great! Good call thank you

Collapse
 
fullstackchris profile image
Chris Frewin

Ah, this is exactly what I've been looking for - I'd love to extend it with configurable options like get vs post, and other things like headers for auth for example. Great!

Collapse
 
techcheck profile image
Andrew

For sure! And thanks! πŸ™

Collapse
 
karunamaymurmu profile image
Karunamay Murmu

How can we call the custom hook on button click event?

Collapse
 
alextrastero profile image
alextrastero

You would need a new hook in that case, one that returns the actual function instead of calling it inside useEffect, something like:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(null);
  const [error, setError] = useState(null);

  const myFetch = useCallback(() => {
    setLoading('loading...')
    setData(null);
    setError(null);
    axios.get(url).then(res => {
      setLoading(false);
      // checking for multiple responses for more flexibility 
      // with the url we send in.
      res.data.content && setData(res.data.content);
      res.content && setData(res.content);
    })
    .catch(err => {
      setLoading(false)
      setError('An error occurred. Awkward..')
    })
  }, [url]);

  return { myFetch, data, loading, error }
}
Enter fullscreen mode Exit fullscreen mode