Replace RxJS with Angular Signals in Pokemon Application

Connie Leung - May 26 '23 - - Dev Community

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:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .