How to convert HTTP Response from Observable to Angular signal with toSignal

Reading Time: 6 minutes

Loading

Introduction

I extended my Pokemon application to call an API to retrieve a Pokemon by id. The HTTP request returned an Observable that required ngIf and async pipe to resolve in order to render the results in inline template. In this blog post, I want to demonstrate how to convert HTTP response to Signal with toSignal. toSignal returns T | undefined but we can provide an initial value to the function to get rid of the undefined type.

let's go

Old Pokemon Component with RxJS codes

// pokemon.component.ts

...omitted import statements for brevity...

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

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [AsyncPipe, FormsModule, NgIf, NgTemplateOutlet],
  template: `
    <h1>
      Display the first 100 pokemon images
    </h1>
    <div>
      <ng-container *ngIf="pokemon$ | async as pokemon">
        <div class="pokemon-container">
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Id: ', value: pokemon.id }"></ng-container>
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Name: ', value: pokemon.name }"></ng-container>
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Height: ', value: pokemon.height }"></ng-container>
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Weight: ', value: pokemon.weight }"></ng-container>
        </div>
        <div class="container">
          <img [src]="pokemon.front_shiny" />
          <img [src]="pokemon.back_shiny" />
        </div>
      </ng-container>
    </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>
      <form #f="ngForm" novalidate>
        <input type="number" [(ngModel)]="searchId" [ngModelOptions]="{ updateOn: 'blur' }" 
          name="searchId" id="searchId" />
      </form>
    </div>
    <ng-template #details let-name let-value="value">
      <label><span style="font-weight: bold; color: #aaa">{{ name }}</span>
        <span>{{ value }}</span>
      </label>
    </ng-template>
  `,
  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>;

  @ViewChild('f', { static: true, read: NgForm })
  myForm: NgForm;

  pokemon$!: Observable<FlattenPokemon>;
  searchId = 1;
  retrievePokemon = retrievePokemonFn();
  
  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);

    const inputId$ = this.myForm.form.valueChanges
      .pipe(
        debounceTime(300),
        distinctUntilChanged((prev, curr) => prev.searchId === curr.searchId),
        filter((form) => form.searchId >= 1 && form.searchId <= 100),
        map((form) => form.searchId),
        map((value) => ({
          value,
          action: POKEMON_ACTION.OVERWRITE,
        }))
      );

    const btnPokemonId$ = merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$, inputId$)
      .pipe(
        scan((acc, { value, action }) => { 
          ... derive pokemon id....
        }, 1),
        startWith(1),
        shareReplay(1),
      );

      this.pokemon$ = btnPokemonId$.pipe(switchMap((id) => this.retrievePokemon(id)));
  }

  createButtonClickObservable(ref: ElementRef<HTMLButtonElement>, value: number) {
    return fromEvent(ref.nativeElement, 'click').pipe(
      map(() => ({ value, action: POKEMON_ACTION.ADD }))
    );
  }
}

retrievePokemonFn() returns a function that accepts an id to retrieve a Pokemon. When btnPokemonId$ Observable emits an id, the stream invokes this.retrievePokemon and assigns the results to this.pokemon$ Observable. Then, this.pokemon$ is resolved in inline template to display image URLs and details. My goals are to refactor ngOnInit and convert this.pokemon$ Observable to Angular signal. Then, inline template renders the signal value instead of the resolved Observable.

Store Reactive results into Behavior Subject

First, I create a BehaviorSubject to store current Pokemon id

// pokemon-component.ts

pokemonIdSub = new BehaviorSubject(1)

Then, I modify inline template to add click event to the button elements to update pokemonIdSub BehaviorSubject.

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)

<button class="btn" *ngFor="let delta of [-2, -1, 1, 2]" (click)="updatePokemonId(delta)">{{delta < 0 ? delta : '+' + delta }}</button>

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) {
    const potentialId = this.pokemonIdSub.getValue() + delta;
    const newId = Math.min(this.max, Math.max(this.min, potentialId));

    this.pokemonIdSub.next(newId);
}

When button is clicked, updatePokemonId sets pokemonIdSub to a value between 1 and 100.

In Imports array, I include NgFor to use ngFor directive

imports: [..., NgFor],

Now, I declare searchIdSub BehaviorSubject to react to changes to number input field

// pokemon.component.ts

searchIdSub = new BehaviorSubject(1);

searchIdSub emits search value, streams to subsequent RxJS operators and subscribes to update pokemonIdSub Behaviour Subject.

Before (RxJS)

ngOnInit() {
    const inputId$ = this.myForm.form.valueChanges
       .pipe(
         debounceTime(300),
         distinctUntilChanged((prev, curr) => prev.searchId === curr.searchId),
         filter((form) => form.searchId >= 1 && form.searchId <= 100),
         map((form) => form.searchId),
         map((value) => ({
           value,
           action: POKEMON_ACTION.OVERWRITE,
         }))
       );
}
After (Signal)

<input type="number" [ngModel]="searchIdSub.getValue()"
   (ngModelChange)="searchIdSub.next($event)"
   name="searchId" id="searchId" />

[(ngModel)] is decomposed to [ngModel] and (ngModelChange) to get my solution to work. NgModel input is bounded to searchIdSub.getValue() and (ngModelChange) updates the BehaviorSubject when input value changes.

constructor() {
    this.searchIdSub
      .pipe(
        debounceTime(300),
        distinctUntilChanged(),
        filter((value) => value >= this.min && value <= this.max),
        takeUntilDestroyed(),
      ).subscribe((value) => this.pokemonIdSub.next(value));
}

Angular 16 introduces takeUntilDestroyed that completes Observable; therefore, I don’t have to implement OnDestroy interface to unsubscribe subscription manually.

Convert Observable to Angular Signal with toSignal

import { toSignal } from '@angular/core/rxjs-interop';

const initialValue: DisplayPokemon = {
  id: 0,
  name: '',
  height: -1,
  weight: -1,
  back_shiny: '',
  front_shiny: '',
};

pokemon = toSignal(
    this.pokemonIdSub.pipe(switchMap((id) => this.retrievePokemon(id))), { initialValue });

When the codes update pokemonIdSub, the BehaviorSubject emits the id to switchMap operator to retrieve the specific Pokemon. The result of the stream is a Pokemon Observable that is passed to toSignal to convert to Angular signal.

pokemon is a signal and I use it to compute rowData signal and pass that signal value to the context object of ngTemplateOutlet.

rowData = 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 }, 
  ] 
});
<div class="pokemon-container">
    <ng-container 
*ngTemplateOutlet="details; context: { $implicit: rowData() }"></ng-container>
</div>

I modify ngTemplate to iterate the rowData array to display the label and actual value.

<ng-template #details let-rowData>
   <label *ngFor="let data of rowData">
      <span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
      <span>{{ data.value }}</span>
  </label>
</ng-template>

I remove NgIf and AsyncPipe from the imports array because inline template does not need them anymore. The final array is consisted of FormsModule, NgTemplateOutlet and NgFor.

imports: [FormsModule, NgTemplateOutlet, NgFor],

New Pokemon Component using toSignal

// retrieve-pokemon.ts
import { HttpClient } from "@angular/common/http";
import { inject } from "@angular/core";
import { map } from "rxjs";
import { DisplayPokemon, Pokemon } from "./interfaces/pokemon.interface";

export const retrievePokemonFn = () => {
  const httpClient = inject(HttpClient);
  return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
    .pipe(
      map((pokemon) => pokemonTransformer(pokemon))
    );
}

const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => ({
  id: pokemon.id,
  name: pokemon.name,
  height: pokemon.height,
  weight: pokemon.weight,
  back_shiny: pokemon.sprites.back_shiny,
  front_shiny: pokemon.sprites.front_shiny,
});
// pokemon.component.ts
...omitted import statements for brevity...

const initialValue: DisplayPokemon = {
  id: 0,
  name: '',
  height: -1,
  weight: -1,
  back_shiny: '',
  front_shiny: '',
};

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [FormsModule, NgTemplateOutlet, NgFor],
  template: `
    <h2>
      Display the first 100 pokemon images
    </h2>
    <div>
      <ng-container>
        <div class="pokemon-container">
          <ng-container *ngTemplateOutlet="details; context: { $implicit: rowData() }"></ng-container>
        </div>
        <div class="container">
          <img [src]="pokemon().front_shiny" />
          <img [src]="pokemon().back_shiny" />
        </div>
      </ng-container>
    </div>
    <div class="container">
      <button class="btn" *ngFor="let delta of [-2, -1, 1, 2]" (click)="updatePokemonId(delta)">{{delta < 0 ? delta : '+' + delta }}</button>
      <input type="number" [ngModel]="searchIdSub.getValue()" (ngModelChange)="searchIdSub.next($event)"
          name="searchId" id="searchId" />
    </div>
    <ng-template #details let-rowData>
      <label *ngFor="let data of rowData">
        <span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
        <span>{{ data.value }}</span>
      </label>
    </ng-template>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  readonly min = 1;
  readonly max = 100;

  searchIdSub = new BehaviorSubject(1);
  retrievePokemon = retrievePokemonFn();
  pokemonIdSub = new BehaviorSubject(1);

  pokemon = toSignal(
    this.pokemonIdSub.pipe(switchMap((id) => this.retrievePokemon(id))), { initialValue });

  rowData = 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 }, 
  ] 
  });

  updatePokemonId(delta: number) {
    const potentialId = this.pokemonIdSub.getValue() + delta;
    const newId = Math.min(this.max, Math.max(this.min, potentialId));

    this.pokemonIdSub.next(newId);
  }
  
  constructor() {
    this.searchIdSub
      .pipe(
        debounceTime(300),
        distinctUntilChanged(),
        filter((value) => value >= this.min && value <= this.max),
        map((value) => Math.floor(value)),
        takeUntilDestroyed(),
      ).subscribe((value) => this.pokemonIdSub.next(value));
  }
}

The new version uses toSignal function to convert Pokemon Observable to Pokemon signal with an initial value. After the conversion, I can use computed to derive rowData signal and pass the signal value to inline template to render. Thus, the inline template and logic is less verbose the previous RxJS version.

This is it and I have enhanced the Pokemon application to make HTTP request and convert HTTP response to signal before signal function is called within inline template to render the results.

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-3
  2. Stackblitz: https://stackblitz.com/edit/angular-sqaafu?file=src%2Fpokemon%2Fpokemon%2Fpokemon.component.ts
  3. PokeAPI: https://pokeapi.co/