
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