All files / src/hooks useApi.ts

100% Statements 19/19
88.88% Branches 8/9
100% Functions 3/3
100% Lines 19/19

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50                                  12x 12x 12x 12x   12x 7x 7x 7x 7x 7x   5x 4x     2x 2x     7x 6x         12x 5x     12x    
import { useState, useEffect, useCallback, useRef } from 'react';
 
interface UseApiState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}
 
/**
 * Generic hook for API calls with loading/error states.
 * Uses a request counter so only the latest fetch wins (prevents race conditions).
 */
export function useApi<T>(
  fetcher: () => Promise<T>,
  deps: unknown[] = [],
): UseApiState<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const requestId = useRef(0);
 
  const fetch = useCallback(async () => {
    const thisRequest = ++requestId.current;
    setLoading(true);
    setError(null);
    try {
      const result = await fetcher();
      // Only apply result if this is still the latest request
      if (thisRequest === requestId.current) {
        setData(result);
      }
    } catch (e) {
      Eif (thisRequest === requestId.current) {
        setError(e instanceof Error ? e.message : 'Unknown error');
      }
    } finally {
      if (thisRequest === requestId.current) {
        setLoading(false);
      }
    }
  }, deps);
 
  useEffect(() => {
    fetch();
  }, [fetch]);
 
  return { data, loading, error, refetch: fetch };
}