useFetch React Hook

Custom React hook to fetch data with loading and error state management.

Fetching data in React often leads to the same boilerplate: track loading state, handle errors, and cancel requests on unmount. A custom useFetch hook encapsulates that logic so your components stay focused on rendering. It takes a URL and dependency list, returning data, error, and loading values.

import { useEffect, useRef, useState } from "react";
 
export function useFetch<T>(url: string, deps: unknown[] = []) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(false);
  const abortRef = useRef<AbortController | null>(null);
 
  useEffect(() => {
    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;
    setLoading(true);
    setError(null);
 
    fetch(url, { signal: controller.signal })
      .then(async (res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = (await res.json()) as T;
        setData(json);
      })
      .catch((err) => {
        if (err.name !== "AbortError") setError(err);
      })
      .finally(() => setLoading(false));
 
    return () => controller.abort();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [url, ...deps]);
 
  return { data, error, loading };
}

Call this hook inside your components: const { data, error, loading } = useFetch<User[]>('/api/users', [userId]);. It automatically cancels in‑flight requests if dependencies change or the component unmounts, preventing memory leaks and race conditions. You can extend this pattern to support retries, caching, or custom headers.