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 ignore flag in the cleanup prevents a stale response from overwriting newer data if the component unmounts or fetchFn changes (see HTTP Requests, Entry 6).

What / Why / When

What

Reusable request state management.

Why

One tested implementation instead of the same try/catch/finally in 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

React
// 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:

Angular
// 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 fetchFn changes, via the dependency array. The Angular function above runs once; for re-fetching when a value changes, use resource() or httpResource(), which track signals automatically.
  • Returning the writable data signal in Angular is the equivalent of returning setFetchedData in React.

Quick reference

ReactAngular
useFetch(fetchFn, initialValue)httpResource() / resource() / custom injectFetch()
useState for each piece of statesignal() for each piece of state
Cleanup with an ignore flagDestroyRef.onDestroy()
Return setFetchedDataReturn the writable signal