Startec

Startec

Replace RxJS with Angular Signals in Pokemon Application

Mai 26, às 08:33

·

6 min de leitura

·

0 leituras

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...
Replace RxJS with Angular Signals in Pokemon Application

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));
 }
}

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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);
 });
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

After (Signal)
<div>
 <label>Pokemon Id:
 <span>{{ currentPokemonId() }}</span>
 </label>
 <div class="container">
 <img [src]="imageUrls().front" />
 <img [src]="imageUrls().back" />
 </div>
</div>

Enter fullscreen mode Exit fullscreen mode

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`
}));

Enter fullscreen mode Exit fullscreen mode

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`
 }));
}

Enter fullscreen mode Exit fullscreen mode

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:


Continue lendo

Hacker News

Atomic Wallet exploited, users report loss of entire portfolios
Several users on Twitter have reported losses of crypto assets, claiming funds held on the Atomic Wallet app vanished. 10886 Total views 75 Total shares Atomic Wallet has been apparently exploited, with...

Hoje, às 01:41

Hacker News

It Will Cost Up to $21.5 Billion to Clean Up California’s Oil Sites. The Industry Won’t Make Enough Money to Pay for It.
An expert used California regulators’ methodology to estimate the cost of cleaning up the state’s onshore oil and gas industry. The study found that cleanup costs will be triple the industry’s projected profits.

Hoje, às 01:01

DEV

How to Create an Evil Twin Access Point; Step-by-Step Guide
Step-by-Step Guide: Creating an Evil Twin An Evil Twin Access Point is a malicious wireless access point that is set up to mimic a legitimate one. It can be used to intercept sensitive information such as...

Jun 3, às 23:41

TabNews

200 anos em 2 mêses: Usando o ChatGPT para auxiliar na criação de um mundo de fantasia. · MarquesJr
Não, eu não pedi pra Gepeto (ChatGPT) criar um mundo fictício de fantasia pra mim e pronto, nem tão pouco incentivo essa prática e também não irei passar uma fórmula mágica. Nesse artigo...

Jun 3, às 23:07

DEV

Atomic Design: A Methodology for Building Design Systems
Introduction Atomic Design is a methodology for creating design systems that recognizes the need to develop thoughtful design systems, rather than creating simple collections of web pages. In this approach,...

Jun 3, às 23:04

Hacker News

Thought Cloning: Learning to Think while Acting by Imitating Human Thinking
Language is often considered a key aspect of human thinking, providing us with exceptional abilities to generalize, explore, plan, replan, and adapt to new situations. However, Reinforcement...

Jun 3, às 23:00

AI | Techcrunch

YouTube rolls back its rules against election misinformation
YouTube was the slowest major platform to disallow misinformation during the 2020 U.S. election and almost three years later, the company will toss that policy out altogether. The company announced Friday...

Jun 3, às 22:57

DEV

Techinical Debt; what is it?
Imagine you're building a house. You want to finish it quickly, so you take some shortcuts along the way. You use low-quality materials, skip some important steps, and don't do thorough testing. The house is...

Jun 3, às 22:45

Marktechpost AI Research News

Researchers From UT Austin and UC Berkeley Introduce Ambient Diffusion: An AI Framework To Train/Finetune Diffusion Models Given Only Corrupted Data As Input
For learning high-dimensional distributions and resolving inverse problems, generative diffusion models are emerging as flexible and potent frameworks. Text conditional foundation models like Dalle-2, Latent...

Jun 3, às 22:40

Hacker News

Scientists may be able to put Mars-bound astronauts into 'suspended animation' using sound waves, mouse study suggests
Ellen Ripley (played by Sigourney Weaver) places herself into suspended animation in the 1979 movie Alien. (Image credit: AJ Pics/Alamy Stock Photo) Scientists have blasted the brains of mice and rats...

Jun 3, às 22:38