Building Custom React HooksEntry 3 of 5

Consuming a Custom Hook

Calling the hook inside a component and destructuring what it returns.

Notes

  • Destructuring with renaming gives the data a meaningful name: fetchedData: userPlaces.
  • The returned setter lets the component change the hook's state, which is useful for optimistic updates.
  • Watch the fetchFn identity. useFetch re-runs its effect whenever fetchFn changes. An imported, module-level function is stable. A function created inside the component is new on every render, which would re-fetch after every render and loop. Wrap such functions in useCallback (see Side Effects, Entry 6).

What / Why / When

What

Using the hook's state and functions in a component.

Why

The component only declares what to fetch and how to display it.

When

Anywhere the hook's logic is needed.

Use case

App loads the user's places and updates them optimistically.

Code compare

React
import { useFetch } from "./hooks/useFetch";import { fetchUserPlaces, updateUserPlaces, type Place } from "./http";​export function App() {  const {    isFetching,    error,    fetchedData: userPlaces,    setFetchedData: setUserPlaces,  } = useFetch(fetchUserPlaces, []); // fetchUserPlaces is module-level: stable​  async function handleSelectPlace(place: Place) {    const previous = userPlaces;    setUserPlaces([place, ...userPlaces]); // optimistic update through the hook's setter​    try {      await updateUserPlaces([place, ...previous]);    } catch {      setUserPlaces(previous); // roll back    }  }​  if (error) return <p className="error">{error}</p>;  if (isFetching) return <p>Fetching your places...</p>;​  return <PlacesList places={userPlaces} onSelect={handleSelectPlace} />;}

With a parameter, stabilize the fetch function with useCallback:

React
import { useCallback } from "react";​type User = { name: string; email: string };​export function UserProfile({ userId }: { userId: string }) {  const fetchUser = useCallback(() => fetchUserById(userId), [userId]); // new only when userId changes  const { fetchedData: user, isFetching, error } = useFetch<User | null>(fetchUser, null);​  if (isFetching) return <p>Loading...</p>;  if (error) return <p>Error: {error}</p>;  if (!user) return null;​  return (    <div>      <h2>{user.name}</h2>      <p>{user.email}</p>    </div>  );}
Angular
import { Component, inject } from "@angular/core";import { firstValueFrom } from "rxjs";import { injectFetch } from "./inject-fetch";import { Place, PlacesService } from "./places.service";​@Component({  selector: "app-root",  template: `    @if (userPlaces.error()) {      <p class="error">{{ userPlaces.error() }}</p>    } @else if (userPlaces.isFetching()) {      <p>Fetching your places...</p>    } @else {      <app-places-list [places]="userPlaces.data()" (select)="selectPlace($event)" />    }  `,})export class AppComponent {  private placesService = inject(PlacesService);​  // Field initializer = injection context  protected userPlaces = injectFetch(    () => firstValueFrom(this.placesService.fetchUserPlaces()),    [] as Place[],  );​  selectPlace(place: Place) {    const previous = this.userPlaces.data();    this.userPlaces.data.set([place, ...previous]); // optimistic update​    this.placesService.updateUserPlaces([place, ...previous]).subscribe({      error: () => this.userPlaces.data.set(previous), // roll back    });  }}

Differences and gotchas

  • Angular has no function-identity problem: injectFetch() runs once per component instance, so there's nothing to stabilize.
  • Angular's service convention still applies: the component calls PlacesService, and only the reusable state logic lives in injectFetch().

Quick reference

ReactAngular
const { fetchedData } = useFetch(fn, [])data = injectFetch(fn, []) in a field initializer
Rename with destructuringAccess data(), isFetching(), error()
useCallback for parameterized fetchFnNot needed; use httpResource() for reactive params