useFetch

useFetch

The useFetch custom hook is a reusable and simplified way of fetching data from an API endpoint in a React application. It encapsulates the logic required to fetch data, manage the state of the data fetching operation, and provides an easy-to-use interface to the consumers of the hook.

Options

  • url: The endpoint to fetch data from.

  • options: (optional) An object that allows for configuring the request such as headers, request method, and other parameters.

Returns

  • data: The data that was fetched from the API endpoint.

  • loading: A boolean flag indicating whether the data is currently being fetched or not.

  • error: An error object that may contain more information about what went wrong with the API call.

  • refetch: A function that can be called to trigger a refetch of the data from the API endpoint.

  • isSuccess: A boolean flag indicating whether the data was successfully fetched and there was no error.

  • isError: A boolean flag indicating whether there was an error while fetching the data.

The Hook

useFetch.ts
import { useReducer, useEffect, useCallback } from "react";
 
interface QueryResult<T> {
  data: T | unknown;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
  isSuccess: boolean;
  isError: boolean;
}
 
interface QueryState<T> {
  data: T | unknown;
  loading: boolean;
  error: Error | null;
}
 
type QueryAction<T> =
  | { type: "FETCH_INIT" }
  | { type: "FETCH_SUCCESS"; payload: T }
  | { type: "FETCH_FAILURE"; payload: Error };
 
function queryReducer<T>(
  state: QueryState<T>,
  action: QueryAction<T>
): QueryState<T> {
  switch (action.type) {
    case "FETCH_INIT":
      return {
        ...state,
        loading: true,
        error: null,
      };
    case "FETCH_SUCCESS":
      return {
        ...state,
        loading: false,
        data: action.payload,
      };
    case "FETCH_FAILURE":
      return {
        ...state,
        loading: false,
        error: action.payload,
      };
    default:
      throw new Error();
  }
}
 
const cache: Record<string, any> = {};
 
export function useFetch<T>(
  url: string,
  options?: RequestInit
): QueryResult<T> {
  const [state, dispatch] = useReducer(queryReducer, {
    data: null,
    loading: true,
    error: null,
  });
 
  const fetchData = useCallback(async () => {
    dispatch({ type: "FETCH_INIT" });
    try {
      const cachedData = cache[url];
      if (cachedData) {
        dispatch({ type: "FETCH_SUCCESS", payload: cachedData });
      } else {
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error(response.statusText);
        }
        const jsonData = (await response.json()) as T;
        cache[url] = jsonData;
        dispatch({ type: "FETCH_SUCCESS", payload: jsonData });
      }
    } catch (err) {
      dispatch({ type: "FETCH_FAILURE", payload: err as Error });
    }
  }, [url, options]);
 
  useEffect(() => {
    fetchData();
  }, [fetchData]);
 
  const refetch = useCallback(() => {
    delete cache[url];
    fetchData();
  }, [url, fetchData]);
 
  const isSuccess = !state.loading && !state.error;
  const isError = !!state.error;
 
  return {
    data: state.data,
    loading: state.loading,
    error: state.error,
    refetch,
    isSuccess,
    isError,
  };
}

Usage

UseFetchDemo.tsx
import { useFetch } from "./useFetch";
 
export default function UseFetchDemo() {
  const { data, loading, isSuccess, refetch, isError, error } = useFetch(
    "https://jsonplaceholder.typicode.com/photos"
  );
 
  return (
    <div>
      {isError ? (
        <div>{error?.message || "something went wrong"}</div>
      ) : (
        <div>
          {loading ? (
            "Loading..."
          ) : (
            <>
              {isSuccess &&
                data?.slice(0, 10)?.map((img) => (
                  <div key={img?.id}>
                    <img src={img?.url} alt="" />
                  </div>
                ))}
            </>
          )}
        </div>
      )}
 
      {isSuccess ? (
        <button type="button" onClick={refetch}>
          Refetch
        </button>
      ) : null}
    </div>
  );
}