Skip to content

Fetch ​

useFetch ​

ts
import { useCallback, useEffect, useRef, useState } from "react";

type Fetcher<T> = (signal?: AbortSignal) => Promise<T>;

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}

/**
 * Custom hook for performing HTTP requests with automatic handling of
 * loading states, errors, and request cancellation.
 *
 * @template T - Type of data returned by the request
 * @param {Fetcher<T>} fetcher - Function that performs the request and returns a Promise
 * @returns {FetchState<T>} Object containing data, loading state, errors, and a refetch function
 *
 * @example
 * ```typescript
 * const { data, loading, error, refetch } = useFetch<User[]>(
 *   async (signal) => {
 *     const response = await fetch('/api/users', { signal });
 *     if (!response.ok) throw new Error('Failed');
 *     return response.json();
 *   }
 * );
 */
export default function useFetch<T>(fetcher: Fetcher<T>): FetchState<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<Error | null>(null);

  const abortController = useRef<AbortController | null>(null);
  const [refetchCount, setRefetchCount] = useState(0);

  const refetch = useCallback(() => {
    setRefetchCount(count => count + 1);
  }, []);

  useEffect(() => {
    if (abortController.current) {
      abortController.current.abort();
    }

    const ctrl = new AbortController();
    abortController.current = ctrl;
    const signal = ctrl.signal;

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      setData(null);

      try {
        const result = await fetcher(signal);
        if (!signal.aborted) {
          setData(result);
        }
      } catch (err) {
        if (err.name === "AbortError") {
          // eslint-disable-next-line no-console
          console.log("Aborted");
          return;
        }
        if (!signal.aborted) {
          setError(err as Error);
        }
      } finally {
        if (!signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      ctrl.abort();
    };
  }, [fetcher, refetchCount]);

  return { data, loading, error, refetch };
}

Released under the MIT License.