typescriptadvanced

useFetch with Cache Hook

Data fetching hook with built-in cache, loading, error states, and automatic revalidation.

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

const cache = new Map<string, { data: unknown; timestamp: number }>();

interface UseFetchOptions {
  cacheTime?: number;
  revalidateOnFocus?: boolean;
}

export function useFetch<T>(url: string, options: UseFetchOptions = {}) {
  const { cacheTime = 60_000, revalidateOnFocus = true } = options;
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);
  const abortRef = useRef<AbortController>();

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

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

    try {
      setLoading(true);
      const res = await fetch(url, { signal: abortRef.current.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);
      setError(null);
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return;
      setError(err instanceof Error ? err : new Error('Fetch failed'));
    } finally {
      setLoading(false);
    }
  };

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

  useEffect(() => {
    if (!revalidateOnFocus) return;
    const onFocus = () => fetchData();
    window.addEventListener('focus', onFocus);
    return () => window.removeEventListener('focus', onFocus);
  }, [url, revalidateOnFocus]);

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

Use Cases

  • API data fetching
  • Dashboard widgets

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.