Introduction
In this blog post, I would like to discuss how signal and HttpClient can retrieve data in Angular. In my observation, I found 5 data retrieval patterns and each one has its pros and cons. When the post analyzes data retrieval in Angular, I hope the readers can choose their preferred choice and apply it to their Angular projects.
The demo passes a signal to Pokemon API to retrieve Pikachu and uses different patterns to display the results. The data retrieval patterns are:
- Signal + HttpClient + AsyncPipe
- Signal + make HttpClient call in effect
- Signal + HttpClient + toObservable + toSignal + SwitchMap
- Signal + computedAsync
- Signal + computedAsync + enable requiredSync to emit sync data
Install ngxtension dependencies
npm i --save-exact ngxtension @use-gesture/vanilla
Install ngxtension
to import computedAsync into the demo.
Bootstrap HttpClient in the application
// main.ts
import { provideHttpClient } from '@angular/common/http';
bootstrapApplication(App, {
providers: [provideHttpClient()]
});
Utility function to retrieve Pikachu
// pokemon.interface.ts
export interface Pokemon {
id: number;
name: string;
sprites: {
front_shiny: string
};
}
export interface DisplayPokemon {
id: number;
name: string;
img: string;
}
// get-pokemon.ts
export const getPokemonFn = (): (id: number) => Observable<DisplayPokemon> => {
const httpClient = inject(HttpClient);
return (id: number) => {
return httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
map((p) => ({
id: p.id,
name: p.name,
img: p.sprites.front_shiny
}))
);
}
}
getPokemonFn
is a high-order function that returns a new function to retrieve Pokemon. The anonymous function accepts a Pokemon ID and makes an HTTP request to retrieve a Pokemon from the Pokemon API.
Signal + HttpClient + AsyncPipe
// main.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [AsyncPipe, PokemonComponent],
template: `
<h3>Signal + AsyncPipe + HttpClient</h3>
@if (pokemon$ | async; as pokemon) {
<app-pokemon [pokemon]="pokemon" />
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
pokemonId = signal(25);
getPokemon = getPokemonFn();
pokemon$ = this.getPokemon(this.pokemonId());
}
App
has a signal with a fixed integer of 25. The component invokes this.getPokemon
and assigns the Observable to pokemon$
. In the inline template, the async pipe resolves pokemon$
to pokemon variable. Then, the variable is passed to the required signal input of PokemonComponent
to display data.
My two cents: I like RxJS and I have no problem using this pattern to retrieve and display data in the inline template. If Angular developers want to avoid AsyncPipe and Observable, they can try the other patterns instead.
Signal + make HTTP call in effect
// main.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [AsyncPipe, PokemonComponent],
template: `
<h3>Signal + make HttpClient call in effect</h3>
@if (pokemon2(); as pokemon2) {
<app-pokemon [pokemon]="pokemon2" />
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
pokemonId = signal(25);
getPokemon = getPokemonFn();
pokemon2 = signal<DisplayPokemon | null>(null);
constructor() {
effect((cleanUp) => {
const subscription = this.getPokemon(this.pokemonId())
.subscribe((p) => this.pokemon2.set(p))
cleanUp(() => subscription.unsubscribe());
});
}
}
In the App
component, I create pokemon2
WritableSignal and initialize it with null. When pokemonId
signal is updated, effect
runs, makes an HTTP request to retrieve the pokemon, and sets the object to pokemon2 in subscribe(). Then, the inline template passes pokemon2 to PokemonComponent to display the data.
My two cents: This pattern uses the signal to store the final result and avoids AsyncPipe in the inline template. However, the component declares a signal explicitly and it could be far from the constructor when the component has many field initializations. Readers scroll up and down the file to find out where pokemon2
is and what value is assigned to it. Moreover, effect
creates a new Observable and a new subscription in each query search. The subscription requires to unsubscribe in the cleanup function to avoid memory leaks. The cleanup part is subtle and developers can easily forget about it.
Let's explore other patterns that do not create subscription
Signal + HttpClient + toObservable + toSignal + SwitchMap
// main.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [AsyncPipe, PokemonComponent],
template: `
<h3>Signal + HttpClient + toObservable + toSignal + SwitchMap</h3>
@if (pokemon3(); as pokemon3) {
<app-pokemon [pokemon]="pokemon3" />
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
pokemonId = signal(25);
getPokemon = getPokemonFn();
pokemon3 = toSignal(
toObservable(this.pokemonId).pipe(switchMap((id) => this.getPokemon(id)))
);
}
toObservable()
exposes pokemonId
signal to an Observable and emits the pokemon id to the switchMap
RxJS operator. The switchMap
operator cancels any unfinished HTTP request and makes a new one to retrieve the Pokemon. The pokemon
Observable is subsequently passed to toSignal to get a signal back. Then, the inline template calls the signal function of pokemon3
and passes the result to PokemonComponent
to display the data.
My two cents: This pattern is good because HttpClient always finishes and unsubscribes. Moreover, switchMap cancels the previous request before making a new one. However, toSignal(toObservable(...))
adds boilerplate codes to the component and the component becomes unmaintainable when this pattern is repeatedly seen. When a root service creates an Observable that does not unsubscribe, then toSignal
can lead to memory leaks. Engineers should use this pattern with care in root services.
Signal + computedAsync
// main.ts
import { computedAsync } from 'ngxtension/computed-async';
@Component({
selector: 'app-root',
standalone: true,
imports: [AsyncPipe, PokemonComponent],
template: `
<h3>Signal + computedAsync</h3>
@if (pokemon4(); as pokemon4) {
<app-pokemon [pokemon]="pokemon4" />
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
pokemonId = signal(25);
getPokemon = getPokemonFn();
pokemon4 = computedAsync(() => this.getPokemon(this.pokemonId()));
}
Use ngxtension's computedAsync
to retrieve the Pokemon via HttpClient.
My two cents: The utility function supports Promise and Observable, and returns a Signal<T | undefined>
. The function uses Subject
to emit value and performs cleanup in the callback of DestroyRef
. The default behavior is switchAll which cancels the previous request. It has all the benefits of the earlier patterns and not the drawbacks. Angular engineers do not have to worry about AsyncPipe, subscriptions that require clean-up, and toSignal(toObservable(...))
boilerplate codes.
Signal + computedAsync + enable requiredSync to emit sync data
// main.ts
import { computedAsync } from 'ngxtension/computed-async';
const DEFAULT_POKEMON: DisplayPokemon = {
id: 0,
name: '',
img: 'https://placehold.co/400',
};
@Component({
selector: 'app-root',
standalone: true,
imports: [AsyncPipe, PokemonComponent],
template: `
<h3>Signal + computedAsync + requireSync = true</h3>
<app-pokemon [pokemon]="pokemon5()" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
pokemonId = signal(25);
getPokemon = getPokemonFn();
pokemon5 = computedAsync(() => this.getPokemon(this.pokemonId())
.pipe(startWith(DEFAULT_POKEMON)),
{
requireSync: true,
}
);
}
This pattern also uses ngxtension's computedAsync
to retrieve the Pokemon via HttpClient. The difference is requireSync option is enabled to emit synchronous data. The signal always has a value and it is impossible to have undefined.
My two cents: By providing the initial value in the startWith
RxJS operator, the return type is Signal<T>
instead of Signal<T | undefined>
. I don't need to use @if to test undefined before displaying the signal value in the App
component.
These are all the patterns that I observe when retrieving data via signal and HttpClient. I prefer to use computedAsync
utility function despite it comes from a third-party library.
My reasoning is as follows:
- No toSignal and toObservable boilerplate codes
- No reference to AsyncPipe and Observable
- No extra declaration of signal. Does not make HTTP requests in effect; therefore, no cleanup of subscription in the cleanup function
The following Stackblitz repo shows the final results:
This is the end of the blog post that analyzes data retrieval patterns in Angular. I hope you like the content and continue to follow my learning experience in Angular, NestJS and other technologies.