Sending HTTP Requests (e.g. Connecting to a Database)Entry 2 of 7
Fetching Data with useEffect
Loading data when a component appears, by running fetch inside an effect.
react.devuseEffect
Notes
- Fetching is a side effect, so it goes in
useEffect. Callingfetchin the component body and then setting state would cause an infinite loop (see Side Effects, Entry 1). - The effect callback can't be
asyncitself, because React expects it to return nothing or a cleanup function, not a promise. Define anasyncfunction inside the effect and call it. - An empty dependency array (
[]) fetches once, after the first render.
What / Why / When
- What
Fetch on mount, store the result in state.
- Why
The component renders immediately, then fills in data when it arrives.
- When
Data needed as soon as a component is shown.
- Use case
Loading the list of available places from the backend.
Code compare
import { useEffect, useState } from "react";type Place = { id: string; title: string };export function AvailablePlaces() { const [availablePlaces, setAvailablePlaces] = useState<Place[]>([]); useEffect(() => { async function fetchPlaces() { const response = await fetch("http://localhost:3000/places"); const resData: { places: Place[] } = await response.json(); setAvailablePlaces(resData.places); } fetchPlaces(); }, []); return ( <ul> {availablePlaces.map((place) => ( <li key={place.id}>{place.title}</li> ))} </ul> );}In Angular, the request lives in a service, not in the component. The component injects the service and only consumes the result.
HttpClient must be provided once in the app config:
// app.config.tsimport { ApplicationConfig } from "@angular/core";import { provideHttpClient, withFetch } from "@angular/common/http";export const appConfig: ApplicationConfig = { providers: [provideHttpClient(withFetch())],};// places.service.tsimport { Injectable, inject } from "@angular/core";import { HttpClient, httpResource } from "@angular/common/http";import { map } from "rxjs";export type Place = { id: string; title: string };const BASE_URL = "http://localhost:3000";@Injectable({ providedIn: "root" })export class PlacesService { private http = inject(HttpClient); // Signal-based (v19.2+): returns a resource with value(), isLoading(), error(). // Call it from a component field initializer or constructor (injection context). loadAvailablePlaces() { return httpResource<{ places: Place[] }>(() => `${BASE_URL}/places`); } // Observable-based (classic) fetchAvailablePlaces() { return this.http .get<{ places: Place[] }>(`${BASE_URL}/places`) .pipe(map((resData) => resData.places)); }}The component, using the signal-based method:
// available-places.component.tsimport { Component, computed, inject } from "@angular/core";import { PlacesService } from "./places.service";@Component({ selector: "app-available-places", template: ` <ul> @for (place of availablePlaces(); track place.id) { <li>{{ place.title }}</li> } </ul> `,})export class AvailablePlacesComponent { private placesResource = inject(PlacesService).loadAvailablePlaces(); availablePlaces = computed(() => this.placesResource.hasValue() ? this.placesResource.value().places : [], );}Or with the classic Observable method, subscribed in ngOnInit:
import { Component, DestroyRef, OnInit, inject, signal } from "@angular/core";import { takeUntilDestroyed } from "@angular/core/rxjs-interop";import { Place, PlacesService } from "./places.service";@Component({ selector: "app-available-places", template: `...` })export class AvailablePlacesComponent implements OnInit { private placesService = inject(PlacesService); private destroyRef = inject(DestroyRef); availablePlaces = signal<Place[]>([]); ngOnInit() { this.placesService .fetchAvailablePlaces() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((places) => this.availablePlaces.set(places)); }}Differences and gotchas
- Component vs service. React examples fetch inside the component because React has no built-in service layer. In Angular, dependency injection makes a service the natural home for HTTP calls, so components stay focused on display.
HttpClientreturns an Observable, which does nothing until you subscribe.fetchreturns a Promise that starts immediately.- Your notes suggested fetching inside Angular's
effect(). PreferhttpResource(),ngOnInit, or a service; the Angular docs discourage usingeffect()for loading data. - In React,
useEffectfetching works but is low-level. Libraries such as TanStack Query, and React 19'suse()with Suspense, handle caching and loading states for you.
Quick reference
| React | Angular |
|---|---|
| Request code in the component | Request code in a service |
fetch inside useEffect(..., []) | Service method (httpResource() or HttpClient) |
| Async function defined inside the effect | Resource signals or Observable subscribe() |
Result stored with useState | Result exposed as signals |