Building Custom React HooksEntry 2 of 5
Building a Custom Hook: useFetch
A generic hook that runs any async fetch function and manages its loading, error, and data state.
Notes
The loading/error/data pattern from the HTTP chapter repeats in every component that fetches. useFetch moves it into one place:
- Parameters make it flexible: the fetch function to run and an initial value.
- Return an object with the state the component needs:
isFetching,error,fetchedData. - Also return the setter (
setFetchedData) so consumers can update the data themselves, for example for optimistic updates. - TypeScript generics (
<T>) let the hook return correctly typed data for any request. - The
ignoreflag in the cleanup prevents a stale response from overwriting newer data if the component unmounts orfetchFnchanges (see HTTP Requests, Entry 6).
What / Why / When
- What
Reusable request state management.
- Why
One tested implementation instead of the same
try/catch/finallyin every component.- When
Two or more components load data the same way.
- Use case
Loading the user's saved places and the list of available places with the same hook.
Code compare
// hooks/useFetch.tsimport { useEffect, useState } from "react";export function useFetch<T>(fetchFn: () => Promise<T>, initialValue: T) { const [fetchedData, setFetchedData] = useState<T>(initialValue); const [isFetching, setIsFetching] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { let ignore = false; async function fetchData() { setIsFetching(true); setError(null); try { const data = await fetchFn(); if (!ignore) setFetchedData(data); } catch (err) { if (!ignore) setError(err instanceof Error ? err.message : "Failed to fetch data."); } finally { if (!ignore) setIsFetching(false); } } fetchData(); return () => { ignore = true; // ignore results from an outdated request }; }, [fetchFn]); return { isFetching, fetchedData, setFetchedData, error };}Angular's built-in resource() and httpResource() already do what useFetch does, so the usual answer is a service method returning httpResource() (see HTTP Requests, Entry 2).
To build the equivalent yourself, write a function that creates signals and uses DestroyRef in place of React's cleanup:
// inject-fetch.tsimport { DestroyRef, inject, signal } from "@angular/core";export function injectFetch<T>(fetchFn: () => Promise<T>, initialValue: T) { const data = signal<T>(initialValue); const isFetching = signal(true); const error = signal<string | null>(null); let destroyed = false; inject(DestroyRef).onDestroy(() => (destroyed = true)); // requires an injection context fetchFn() .then((result) => { if (!destroyed) data.set(result); }) .catch((err) => { if (!destroyed) error.set(err instanceof Error ? err.message : "Failed to fetch data."); }) .finally(() => { if (!destroyed) isFetching.set(false); }); return { data, // writable, like returning setFetchedData isFetching: isFetching.asReadonly(), error: error.asReadonly(), };}Differences and gotchas
- The React hook re-runs when
fetchFnchanges, via the dependency array. The Angular function above runs once; for re-fetching when a value changes, useresource()orhttpResource(), which track signals automatically. - Returning the writable
datasignal in Angular is the equivalent of returningsetFetchedDatain React.
Quick reference
| React | Angular |
|---|---|
useFetch(fetchFn, initialValue) | httpResource() / resource() / custom injectFetch() |
useState for each piece of state | signal() for each piece of state |
Cleanup with an ignore flag | DestroyRef.onDestroy() |
Return setFetchedData | Return the writable signal |