typescriptintermediate

React Custom Hook for Data Fetching

A reusable useFetch hook with loading states, error handling, caching, and abort support.

typescript
import { useState, useEffect, useRef, useCallback } from 'react';

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

const cache = new Map<string, { data: unknown; timestamp: number }>();
const CACHE_TTL = 60_000; // 1 minute

export function useFetch<T>(
  url: string,
  options?: RequestInit & { cacheTtl?: number }
): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const abortRef = useRef<AbortController | null>(null);
  const ttl = options?.cacheTtl ?? CACHE_TTL;

  const fetchData = useCallback(async () => {
    // Check cache
    const cached = cache.get(url);
    if (cached && Date.now() - cached.timestamp < ttl) {
      setData(cached.data as T);
      setLoading(false);
      return;
    }

    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;

    setLoading(true);
    setError(null);

    try {
      const res = await fetch(url, { ...options, signal: controller.signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json = await res.json();
      cache.set(url, { data: json, timestamp: Date.now() });
      setData(json);
    } catch (err) {
      if ((err as Error).name !== 'AbortError') {
        setError((err as Error).message);
      }
    } finally {
      setLoading(false);
    }
  }, [url, ttl]);

  useEffect(() => {
    fetchData();
    return () => abortRef.current?.abort();
  }, [fetchData]);

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

// Usage
function UserList() {
  const { data, error, loading, refetch } = useFetch<{ id: number; name: string }[]>('/api/users');
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error} <button onClick={refetch}>Retry</button></div>;
  return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Use Cases

  • Reusable data fetching across components
  • Client-side caching with abort on unmount
  • Error handling and retry for API calls

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.