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 };
}