Introduction
In this blog post, I would like to convert "Service with a Subject" to "Service with a Signal " and expose signals only. It is made possible by calling toSignal
to convert Observable to signal. Then, I can pass signal values to Angular components to display data. After using signal values directly in the application, inline templates don't have to use async pipe to resolve Observable. Moreover, imports array of the components do not need to NgIf
and AsyncPipe
.
Source codes of "Service with a Subject"
// pokemon.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class PokemonService {
private readonly pokemonIdSub = new Subject<number>();
readonly pokemonId$ = this.pokemonIdSub.asObservable();
updatePokemonId(pokemonId: number) {
this.pokemonIdSub.next(pokemonId);
}
}
// pokemon.http.ts
export const retrievePokemonFn = () => {
const httpClient = inject(HttpClient);
return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
map((pokemon) => ({
id: pokemon.id,
name: pokemon.name,
height: pokemon.height,
weight: pokemon.weight,
back_shiny: pokemon.sprites.back_shiny,
front_shiny: pokemon.sprites.front_shiny,
abilities: pokemon.abilities.map((ability) => ({
name: ability.ability.name,
is_hidden: ability.is_hidden
})),
stats: pokemon.stats.map((stat) => ({
name: stat.stat.name,
effort: stat.effort,
base_stat: stat.base_stat,
})),
}))
);
}
export const getPokemonId = () => inject(PokemonService).pokemonId$;
// pokemon.component.ts
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [AsyncPipe, NgIf, PokemonControlsComponent, PokemonAbilitiesComponent, PokemonStatsComponent, PokemonPersonalComponent],
template: `
<h1>
Display the first 100 pokemon images
</h1>
<div>
<ng-container *ngIf="pokemon$ | async as pokemon">
<div class="container">
<img [src]="pokemon.front_shiny" />
<img [src]="pokemon.back_shiny" />
</div>
<app-pokemon-personal [pokemon]="pokemon"></app-pokemon-personal>
<app-pokemon-stats [stats]="pokemon.stats"></app-pokemon-stats>
<app-pokemon-abilities [abilities]="pokemon.abilities"></app-pokemon-abilities>
</ng-container>
</div>
<app-pokemon-controls></app-pokemon-controls>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
retrievePokemon = retrievePokemonFn();
pokemon$ = getPokemonId().pipe(switchMap((id) => this.retrievePokemon(id)));
}
PokemonService
encapsulates pokemonIdSub
subject and exposes pokemonId$
Observable. In PokemonComponent
, I invoke retrievePokemon
function to retrieve a new Pokemon whenever pokemonId$
emits a new id. pokemon$
is a Pokemon Observable that I resolve in the inline template in order to assign the Pokemon object to child components.
Next, I am going to convert PokemonService
from "Service with a Subject" to "Service with a Signal" to highlight the benefits of using signals.
Conversion to "Service with a Signal"
First, I combine pokemon.http.ts
and pokemon.service.ts
to move retrievePokemonFn
to the service.
// pokemon.service.ts
// Point 1: move helper functions to this service
const retrievePokemonFn = () => {
const httpClient = inject(HttpClient);
return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`);
}
const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => {
const stats = pokemon.stats.map((stat) => ({
name: stat.stat.name,
effort: stat.effort,
baseStat: stat.base_stat,
}));
const abilities = pokemon.abilities.map((ability) => ({
name: ability.ability.name,
isHidden: ability.is_hidden
}));
const { id, name, height, weight, sprites } = pokemon;
return {
id,
name,
height,
weight,
backShiny: sprites.back_shiny,
frontShiny: sprites.front_shiny,
abilities,
stats,
}
}
@Injectable({
providedIn: 'root'
})
export class PokemonService {
private readonly pokemonId = signal(1);
private readonly retrievePokemon = retrievePokemonFn()
pokemon$ = toObservable(this.pokemonId).pipe(
switchMap((id) => this.retrievePokemon(id)),
map((pokemon) => pokemonTransformer(pokemon)),
);
updatePokemonId(input: PokemonDelta | number) {
if (typeof input === 'number') {
this.pokemonId.set(input);
} else {
this.pokemonId.update((value) => {
const newId = value + input.delta;
return Math.min(input.max, Math.max(input.min, newId));
});
}
}
}
pokemonId
is a signal that stores Pokemon id. When toObservable(this.pokemonId)
emits an id, the Observable invokes this.retrievePokemon
to retrieve a Pokemon and pokemonTransformer
to transform the data. .
Next, I am going to modify components to use signals instead of Observable.
Modify Pokemon Component to use signals
const initialValue: DisplayPokemon = {
id: -1,
name: '',
height: 0,
weight: 0,
backShiny: '',
frontShiny: '',
abilities: [],
stats: [],
}
export class PokemonComponent {
service = inject(PokemonService);
// Point 2: convert Observable to signal using toSignal
pokemon = toSignal(this.pokemonService.pokemon$, { initialValue });
// Point 3: compute a signal from an existing signal
personalData = computed(() => {
const { id, name, height, weight } = this.pokemon();
return [
{ text: 'Id: ', value: id },
{ text: 'Name: ', value: name },
{ text: 'Height: ', value: height },
{ text: 'Weight: ', value: weight },
];
});
}
PokemonComponent
injects PokemonService
to access pokemon$
Observable. Then, toSignal
converts the Pokemon Observable to a Pokemon signal.
personalData
is a computed signal that derives from this.pokemon()
signal value. It is a signal that returns the id, name, height and weight of a Pokemon
Without the pokemon$
Observable, I revise the inline template to render signal values and pass signal values to children components.
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [PokemonControlsComponent, PokemonAbilitiesComponent, PokemonStatsComponent, PokemonPersonalComponent],
template: `
<h2>
Display the first 100 pokemon images
</h2>
<div>
<ng-container>
<div class="container">
<img [src]="pokemon().frontShiny" />
<img [src]="pokemon().backShiny" />
</div>
<app-pokemon-personal [personalData]="personalData()" />
<app-pokemon-stats [stats]="pokemon().stats" />
<app-pokemon-abilities [abilities]="pokemon().abilities" />
</ng-container>
</div>
<app-pokemon-controls />
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent { ... }
One obvious change is the inline template eliminates ngContainer, ngIf and async pipe. It also leads to the removal of AsyncPipe
and NgIf
from the imports array.
The inline template invokes pokemon()
multiple times to access frontShiny, backShiny, stats and abilities properties. stats and abilities subsequently become the inputs of PokemonStatsComponent
and PokemonAbilitiesComponent
respectively.
Similarly, the result of personalData()
is passed to personalData
input of PokemonPersonalComponent
.
Modify child components to accept signal value input
The application breaks after code changes in PokemonComponent
. It is because the input of PokemonPersonalComponent
has different type. In order to fix the problem, I correct the input value of the child component.
// pokemon-personal.component.ts
@Component({
selector: 'app-pokemon-personal',
standalone: true,
imports: [NgTemplateOutlet, NgFor],
template:`
<div class="pokemon-container" style="padding: 0.5rem;">
<ng-container *ngTemplateOutlet="details; context: { $implicit: personalData }"></ng-container>
</div>
<ng-template #details let-personalData>
<label *ngFor="let data of personalData">
<span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
<span>{{ data.value }}</span>
</label>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonPersonalComponent {
@Input({ required: true })
personalData: ({ text: string; value: string; } | { text: string; value: number })[];
}
I replace pokemon
input with personalData
and use the latter in the inline template to render array values.
If I use Observable in PokemonComponent
, I cannot construct personalData
in a reactive manner. I would subscribe Pokemon Observable and construct personaData
in the callback. Furthermore, I complete the Observable using takeUntilDestroyed
to prevent memory leak.
This is it and I have converted the Pokemon service from "Service with a Subject" to "Service with a Signal". The Pokemon service encapsulates HTTP call, converts Observable to signal and exposes signals to outside. In components, I call signal functions within inline templates to display their values. Moreover, the components stop importing NgIf
and AsyncPipe
because they don't need to resolve Observable.
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.