SyntaxStudy
Sign Up
React useFetch: Data Fetching as a Custom Hook
React Beginner 1 min read

useFetch: Data Fetching as a Custom Hook

Extracting data fetching into a useFetch custom hook eliminates duplicated useEffect and useState boilerplate from every component that loads data. The hook accepts a URL, manages loading, data, and error state internally, and returns them to the caller. Adding an AbortController inside the effect and calling abort in the cleanup function cancels in-flight requests when the component unmounts or the URL changes, preventing stale state updates. You can extend the hook to accept an options object for non-GET requests, refresh callbacks, or caching layers. However, for production applications with complex fetching needs, the community has converged on libraries like TanStack Query and SWR, which handle caching, background refetching, deduplication, and optimistic updates out of the box. Understanding how to build useFetch from scratch is still valuable: it teaches you what these libraries do under the hood and gives you the skills to build lightweight fetching utilities when adding a full library is not warranted.
Example
import { useState, useEffect, useCallback } from 'react';

function useFetch(url, options = {}) {
    const [data,    setData   ] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error,   setError  ] = useState(null);

    const fetchData = useCallback(async (signal) => {
        setLoading(true);
        setError(null);
        try {
            const res = await fetch(url, { ...options, signal });
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            const json = await res.json();
            setData(json);
        } catch (err) {
            if (err.name !== 'AbortError') setError(err.message);
        } finally {
            setLoading(false);
        }
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [url]);

    useEffect(() => {
        const controller = new AbortController();
        fetchData(controller.signal);
        return () => controller.abort();
    }, [fetchData]);

    return { data, loading, error, refetch: () => fetchData(new AbortController().signal) };
}

// ── Usage ──────────────────────────────────────────────────────────────────
function PostList() {
    const { data: posts, loading, error, refetch } = useFetch('/api/posts');

    if (loading) return <p>Loading…</p>;
    if (error)   return <p>Error: {error} <button onClick={refetch}>Retry</button></p>;

    return (
        <ul>
            {posts.map(p => <li key={p.id}>{p.title}</li>)}
        </ul>
    );
}