Replace RxJS with Angular Signals in Pokemon Application

Reading Time: 4 minutes

Loading

Introduction

I wrote the 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
let's go

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:

  1. Github Repo: https://github.com/railsstudent/ng-pokemon-signal/tree/main/projects/pokemon-signal-demo-1
  2. Stackblitz: https://stackblitz.com/edit/angular-yd67rk?file=src%2Fpokemon%2Fpokemon%2Fpokemon.component.ts
  3. Youtube: https://youtu.be/88X3jaAobDQ
  4. PokeAPI: https://pokeapi.co/