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
fetchFnidentity.useFetchre-runs its effect wheneverfetchFnchanges. 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 inuseCallback(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
Apploads the user's places and updates them optimistically.
Code compare
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:
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> );}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 ininjectFetch().
Quick reference
| React | Angular |
|---|---|
const { fetchedData } = useFetch(fn, []) | data = injectFetch(fn, []) in a field initializer |
| Rename with destructuring | Access data(), isFetching(), error() |
useCallback for parameterized fetchFn | Not needed; use httpResource() for reactive params |