logo

Nurav UI

Community
Hooks

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.

Loading...

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 AbortController to 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)

Terminal
npx shadcn@latest add @nurav-ui/use-fetch

Alternatively, install via direct URL:

Terminal
npx shadcn@latest add https://nurav-ui.vercel.app/r/use-fetch.json

Manual Setup

Create the hook file

Copy the source into hooks/use-fetch.ts:

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

PropTypeDefaultDescription
urlrequired
string | null—The endpoint to fetch from. Passing `null` stops the effect.
options
RequestInit—Standard `fetch` configuration (headers, method, etc).

Return Value

PropTypeDefaultDescription
data
T | null—The parsed JSON response.
isLoading
boolean—`true` while the request is pending.
error
Error | null—Any error object caught during the process.
refetch
() => void—Triggers the fetch manually.

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.

Built by Varun

Last Updated:April 10, 2026

On this page