DEV Community

Cover image for React Custom Hook - useFetch

React Custom Hook - useFetch

Andrew on April 24, 2021

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....
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
 
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
 
dhan46code profile image
dhan46code

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

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
Collapse
 
newtfrank profile image
Newton
function useFetch(url, disableFetchOnMount=false) {
  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]);

  useEffect(()=>{
     if(disableFetchOnMount) return
     myFetch()
  },[disabledFetchOnMount, myFetch])

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

You could also add a parameter to disable the fetch on mount instead of creating a whole new hook.

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
 
ag_developer profile image
Agile Solutions Developer

Please fix code issue, close useFetch function correctly, add "}" this character to end of line