Replace RxJS with Angular Signals in Pokemon Application
Connie Leung
Posted on May 26, 2023
Introduction
I wrote a simple Pokemon application in Angular 15 and RxJS to display image URLs of a specific Pokemon. In this use case, I would like to replace RxJS with Angular signals to simplify reactive codes. When code refactoring completes, ngOnInit method does not have any RxJs code and can delete. Moreover, ViewChild is redundant and no more NgIf and AsyncPipe imports.
Steps will be as follows:
- Create a signal to store current Pokemon id
- Create a computed signal that builds the image URLs of the Pokemon
- Update inline template to use signal and computed signal instead
- Delete NgIf and AsyncPipe imports
Old Pokemon Component with RxJS codes
// pokemon.component.ts
...omitted import statements...
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [AsyncPipe, NgIf],
template: `
<h1>
Display the first 100 pokemon images
</h1>
<div>
<label>Pokemon Id:
<span>{{ btnPokemonId$ | async }}</span>
</label>
<div class="container" *ngIf="images$ | async as images">
<img [src]="images.frontUrl" />
<img [src]="images.backUrl" />
</div>
</div>
<div class="container">
<button class="btn" #btnMinusTwo>-2</button>
<button class="btn" #btnMinusOne>-1</button>
<button class="btn" #btnAddOne>+1</button>
<button class="btn" #btnAddTwo>+2</button>
</div>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent implements OnInit {
@ViewChild('btnMinusTwo', { static: true, read: ElementRef })
btnMinusTwo!: ElementRef<HTMLButtonElement>;
@ViewChild('btnMinusOne', { static: true, read: ElementRef })
btnMinusOne!: ElementRef<HTMLButtonElement>;
@ViewChild('btnAddOne', { static: true, read: ElementRef })
btnAddOne!: ElementRef<HTMLButtonElement>;
@ViewChild('btnAddTwo', { static: true, read: ElementRef })
btnAddTwo!: ElementRef<HTMLButtonElement>;
btnPokemonId$!: Observable<number>;
images$!: Observable<{ frontUrl: string, backUrl: string }>;
ngOnInit() {
const btnMinusTwo$ = this.createButtonClickObservable(this.btnMinusTwo, -2);
const btnMinusOne$ = this.createButtonClickObservable(this.btnMinusOne, -1);
const btnAddOne$ = this.createButtonClickObservable(this.btnAddOne, 1);
const btnAddTwo$ = this.createButtonClickObservable(this.btnAddTwo, 2);
this.btnPokemonId$ = merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$)
.pipe(
scan((acc, value) => {
const potentialValue = acc + value;
if (potentialValue >= 1 && potentialValue <= 100) {
return potentialValue;
} else if (potentialValue < 1) {
return 1;
}
return 100;
}, 1),
startWith(1),
shareReplay(1),
);
const pokemonBaseUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
this.images$ = this.btnPokemonId$.pipe(
map((pokemonId: number) => ({
frontUrl: `${pokemonBaseUrl}/shiny/${pokemonId}.png`,
backUrl: `${pokemonBaseUrl}/back/shiny/${pokemonId}.png`
}))
);
}
createButtonClickObservable(ref: ElementRef<HTMLButtonElement>, value: number) {
return fromEvent(ref.nativeElement, 'click').pipe(map(() => value));
}
}
I will rewrite the Pokemon component to replace RxJS with Angular signals, make ngOnInit useless and delete it.
First, I create a signal to store current Pokemon id
// pokemon-component.ts
currentPokemonId = signal(1);
Then, I modify inline template to add click event to the button elements to update currentPokemonId signal.
Before (RxJS)
<div class="container">
<button class="btn" #btnMinusTwo>-2</button>
<button class="btn" #btnMinusOne>-1</button>
<button class="btn" #btnAddOne>+1</button>
<button class="btn" #btnAddTwo>+2</button>
</div>
After (Signal)
<div class="container">
<button class="btn" (click)="updatePokemonId(-2)">-2</button>
<button class="btn" (click)="updatePokemonId(-1)">-1</button>
<button class="btn" (click)="updatePokemonId(1)">+1</button>
<button class="btn" (click)="updatePokemonId(2)">+2</button>
</div>
In signal version, I remove template variables such that the component does not require ViewChild to query HTMLButtonElement
readonly min = 1;
readonly max = 100;
updatePokemonId(delta: number) {
this.currentPokemonId.update((pokemonId) => {
const newId = pokemonId + delta;
return Math.min(Math.max(this.min, newId), this.max);
});
}
When button is clicked, updatePokemonId
sets currentPokemonId
to a value between 1 and 100.
Next, I further modify inline template to replace images$
Observable with imageUrls
computed signal and btnPokemonId$
Observable with currentPokemonId
Before (RxJS)
<div>
<label>Pokemon Id:
<span>{{ btnPokemonId$ | async }}</span>
</label>
<div class="container" *ngIf="images$ | async as images">
<img [src]="images.frontUrl" />
<img [src]="images.backUrl" />
</div>
</div>
After (Signal)
<div>
<label>Pokemon Id:
<span>{{ currentPokemonId() }}</span>
</label>
<div class="container">
<img [src]="imageUrls().front" />
<img [src]="imageUrls().back" />
</div>
</div>
In signal version, I invoke currentPokemonId()
to display the current Pokemon id. imageUrls
is a computed signal that returns front and back URLs of pokemon.
const pokemonBaseUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
imageUrls = computed(() => ({
front: `${pokemonBaseUrl}/shiny/${this.currentPokemonId()}.png`,
back: `${pokemonBaseUrl}/back/shiny/${this.currentPokemonId()}.png`
}));
After applying these changes, the inline template does not rely on async pipe and ngIf and they can be removed from imports array.
New Pokemon Component using Angular Signals
// pokemon.component.ts
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
const pokemonBaseUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
@Component({
selector: 'app-pokemon',
standalone: true,
template: `
<h2>
Use Angular Signal to display the first 100 pokemon images
</h2>
<div>
<label>Pokemon Id:
<span>{{ currentPokemonId() }}</span>
</label>
<div class="container">
<img [src]="imageUrls().front" />
<img [src]="imageUrls().back" />
</div>
</div>
<div class="container">
<button class="btn" (click)="updatePokemonId(-2)">-2</button>
<button class="btn" (click)="updatePokemonId(-1)">-1</button>
<button class="btn" (click)="updatePokemonId(1)">+1</button>
<button class="btn" (click)="updatePokemonId(2)">+2</button>
</div>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
readonly min = 1;
readonly max = 100;
currentPokemonId = signal(1);
updatePokemonId(delta: number) {
this.currentPokemonId.update((pokemonId) => {
const newId = pokemonId + delta;
return Math.min(Math.max(this.min, newId), this.max);
});
}
imageUrls = computed(() => ({
front: `${pokemonBaseUrl}/shiny/${this.currentPokemonId()}.png`,
back: `${pokemonBaseUrl}/back/shiny/${this.currentPokemonId()}.png`
}));
}
The new version has zero dependency of RxJS codes, does not implement NgOnInit interface and ngOnInit
method. It deletes many lines of codes to become easier to read and understand.
This is it and I have rewritten the Pokemon application to replace RxJS with Angular signals.
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.
Resources:
Posted on May 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.