Privacy Policy
Mon Dec 15
04:05

About

Blog

Github

GitHub

Github

YouTube

Github

Instagram

LinkedIn

LinkedIn

~/Users/jan/blog/custom-react-hook-fetch-loading-error

back

Custom React Hook for Fetch with Loading and Error States

Custom React Hook for Fetch with Loading and Error States
Custom React Hook for Fetch with Loading and Error States

A reusable useFetch hook that manages loading, error, and data states—eliminating the need to repeat the same useState pattern across every data-fetching component.

The Problem

Developers consistently write identical state management code for API calls. The pattern involves three useState declarations repeated throughout an application, creating unnecessary boilerplate and maintenance overhead.

The Hook Solution

Here's a TypeScript-based custom hook that encapsulates fetch logic:

interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
export function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}

Usage Example

Components become significantly cleaner with this abstraction:

function UserList() {
const { data, loading, error, refetch } = useFetch<User[]>('/api/users');
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
return (
<div>
<button onClick={refetch}>Refresh</button>
{data?.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}

Extended Options

The hook can be expanded to support custom headers and HTTP methods:

interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
}

When to Use Alternatives

The solution excels for simple scenarios. For complex applications requiring caching, optimistic updates, or global synchronization, libraries like React Query or SWR offer superior capabilities.

Key Takeaways

  • Extract repetitive logic into custom hooks for reusability
  • Provide a refetch mechanism for manual state updates
  • Leverage TypeScript generics for type-safe response handling
  • Remember that fetch doesn't automatically throw errors on HTTP failures—explicit checking is necessary