useFetch
A scalable and performant hook for fetching data. Standardizes API calls with built-in loading states, error handling, and request cancellation.Copy Markdown
Preview
Experience useFetch in action with our Daily Jokes demo. It handles fetching, loading states (using Skeleton), and manual refetching seamlessly.
Overview
useFetch simplifies the common pattern of data fetching in React. Instead of manually managing three pieces of state (data, loading, error) and cleanup logic, you gain a clean, declarative API for your network requests.
Features:
- Automatic Cleanup: Uses
AbortControllerto cancel pending requests when the component unmounts or the URL changes. - Type Safety: Fully generic, allowing you to define exactly what data structure you expect.
- Manual Refetching: Returns a helper function to trigger the fetch again (perfect for "Try Again" buttons).
- SSR Safe: Designed to handle environment differences gracefully.
Installation
Automatic (CLI)
npx shadcn@latest add @nurav-ui/use-fetchAlternatively, install via direct URL:
npx shadcn@latest add https://nurav-ui.vercel.app/r/use-fetch.jsonManual Setup
Create the hook file
Copy the source into hooks/use-fetch.ts:
import { useState, useEffect, useRef, useCallback } from "react";
export function useFetch<T>(url: string | null, options?: RequestInit) {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(!!url);
const [error, setError] = useState<Error | null>(null);
const optionsRef = useRef(options);
optionsRef.current = options;
const fetchData = useCallback(async (abortController: AbortController) => {
if (!url) return;
setIsLoading(true);
setError(null);
try {
const response = await fetch(url, {
...optionsRef.current,
signal: abortController.signal,
});
if (!response.ok) throw new Error(`Error: ${response.status}`);
setData((await response.json()) as T);
} catch (err: any) {
if (err.name === "AbortError") return;
setError(err instanceof Error ? err : new Error(String(err)));
} finally {
setIsLoading(false);
}
}, [url]);
useEffect(() => {
const abortController = new AbortController();
if (url) fetchData(abortController);
else setIsLoading(false);
return () => abortController.abort();
}, [url, fetchData]);
return { data, isLoading, error, refetch: () => fetchData(new AbortController()) };
}Usage
Basic GET Request
Fetching a list of users from a public API.
import { useFetch } from '@/hooks/use-fetch';
interface User {
id: number;
name: string;
email: string;
}
export default function UserList() {
const { data, isLoading, error } = useFetch<User[]>('https://jsonplaceholder.typicode.com/users');
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul className="space-y-2">
{data?.map(user => (
<li key={user.id} className="p-3 border rounded-lg">
{user.name} ({user.email})
</li>
))}
</ul>
);
}Refetching Data
Using the refetch function to reload data manually.
import { useFetch } from '@/hooks/use-fetch';
export default function RandomQuote() {
const { data, isLoading, error, refetch } = useFetch<{ content: string }>('https://api.quotable.io/random');
return (
<div className="p-6 border rounded-xl bg-muted/50 text-center">
{isLoading ? (
<p>Fetching wisdom...</p>
) : error ? (
<p className="text-destructive">Failed to load quote.</p>
) : (
<blockquote className="text-xl italic">"{data?.content}"</blockquote>
)}
<button
onClick={refetch}
disabled={isLoading}
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-md disabled:opacity-50"
>
New Quote
</button>
</div>
);
}Conditional Fetching
The hook only fetches when a valid URL string is provided. You can pass null to pause fetching.
import { useState } from 'react';
import { useFetch } from '@/hooks/use-fetch';
export default function SearchDetails() {
const [selectedId, setSelectedId] = useState<string | null>(null);
// Only fetches when selectedId is NOT null
const { data, isLoading } = useFetch(
selectedId ? `https://api.example.com/items/${selectedId}` : null
);
return (
<div>
<button onClick={() => setSelectedId('123')}>Load Item 123</button>
{isLoading && <p>Loading details...</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}API Reference
Parameters
Return Value
Race Conditions: This hook handles race conditions via AbortController. If you switch URLs quickly, only the response from the latest URL will be applied to the state.