Analyze ways to retrieve data with signals and HttpClient in Angular

Connie Leung - Feb 15 - - Dev Community

Introduction

In this blog post, I would like to discuss how signal and HttpClient can retrieve data in Angular. In my observation, I found 5 data retrieval patterns and each one has its pros and cons. When the post analyzes data retrieval in Angular, I hope the readers can choose their preferred choice and apply it to their Angular projects.

The demo passes a signal to Pokemon API to retrieve Pikachu and uses different patterns to display the results. The data retrieval patterns are:

  1. Signal + HttpClient + AsyncPipe
  2. Signal + make HttpClient call in effect
  3. Signal + HttpClient + toObservable + toSignal + SwitchMap
  4. Signal + computedAsync
  5. Signal + computedAsync + enable requiredSync to emit sync data

Install ngxtension dependencies

npm i --save-exact ngxtension @use-gesture/vanilla
Enter fullscreen mode Exit fullscreen mode

Install ngxtension to import computedAsync into the demo.

Bootstrap HttpClient in the application

// main.ts

import { provideHttpClient } from '@angular/common/http';

bootstrapApplication(App, {
  providers: [provideHttpClient()]
});
Enter fullscreen mode Exit fullscreen mode

Utility function to retrieve Pikachu

// pokemon.interface.ts

export interface Pokemon {
  id: number;
  name: string;
  sprites: {
    front_shiny: string
  };
}

export interface DisplayPokemon {
  id: number;
  name: string;
  img: string;
}

// get-pokemon.ts

export const getPokemonFn = (): (id: number) => Observable<DisplayPokemon> => {
  const httpClient = inject(HttpClient);

  return (id: number) => {
    return httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
      .pipe(
        map((p) => ({
          id: p.id,
          name: p.name,
          img: p.sprites.front_shiny
        }))
      );
  }
}
Enter fullscreen mode Exit fullscreen mode

getPokemonFn is a high-order function that returns a new function to retrieve Pokemon. The anonymous function accepts a Pokemon ID and makes an HTTP request to retrieve a Pokemon from the Pokemon API.

Signal + HttpClient + AsyncPipe

// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe, PokemonComponent],
  template: `
    <h3>Signal + AsyncPipe + HttpClient</h3>
    @if (pokemon$ | async; as pokemon) {
      <app-pokemon [pokemon]="pokemon" />
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  pokemonId = signal(25);
  getPokemon = getPokemonFn();

  pokemon$ = this.getPokemon(this.pokemonId());
}
Enter fullscreen mode Exit fullscreen mode

App has a signal with a fixed integer of 25. The component invokes this.getPokemon and assigns the Observable to pokemon$. In the inline template, the async pipe resolves pokemon$ to pokemon variable. Then, the variable is passed to the required signal input of PokemonComponent to display data.

My two cents:  I like RxJS and I have no problem using this pattern to retrieve and display data in the inline template. If Angular developers want to avoid AsyncPipe and Observable, they can try the other patterns instead.

Signal + make HTTP call in effect

// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe, PokemonComponent],
  template: `
    <h3>Signal + make HttpClient call in effect</h3>
    @if (pokemon2(); as pokemon2) {
      <app-pokemon [pokemon]="pokemon2" />
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  pokemonId = signal(25);
  getPokemon = getPokemonFn();

  pokemon2 = signal<DisplayPokemon | null>(null);

  constructor() {
    effect((cleanUp) => {
      const subscription = this.getPokemon(this.pokemonId())
        .subscribe((p) => this.pokemon2.set(p))

      cleanUp(() => subscription.unsubscribe());
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

In the App component, I create pokemon2 WritableSignal and initialize it with null. When pokemonId signal is updated, effect runs, makes an HTTP request to retrieve the pokemon, and sets the object to pokemon2 in subscribe(). Then, the inline template passes pokemon2 to PokemonComponent to display the data.

My two cents:  This pattern uses the signal to store the final result and avoids AsyncPipe in the inline template. However, the component declares a signal explicitly and it could be far from the constructor when the component has many field initializations. Readers scroll up and down the file to find out where pokemon2 is and what value is assigned to it. Moreover, effect creates a new Observable and a new subscription in each query search. The subscription requires to unsubscribe in the cleanup function to avoid memory leaks. The cleanup part is subtle and developers can easily forget about it.

Let's explore other patterns that do not create subscription

Signal + HttpClient + toObservable + toSignal + SwitchMap

// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe, PokemonComponent],
  template: `
    <h3>Signal + HttpClient + toObservable + toSignal + SwitchMap</h3>
    @if (pokemon3(); as pokemon3) {
      <app-pokemon [pokemon]="pokemon3" />
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  pokemonId = signal(25);
  getPokemon = getPokemonFn();

  pokemon3 = toSignal(
    toObservable(this.pokemonId).pipe(switchMap((id) => this.getPokemon(id)))  
  );
}
Enter fullscreen mode Exit fullscreen mode

toObservable() exposes pokemonId signal to an Observable and emits the pokemon id to the switchMap RxJS operator. The switchMap operator cancels any unfinished HTTP request and makes a new one to retrieve the Pokemon. The pokemon Observable is subsequently passed to toSignal to get a signal back. Then, the inline template calls the signal function of pokemon3 and passes the result to PokemonComponent to display the data.

My two cents: This pattern is good because HttpClient always finishes and unsubscribes. Moreover, switchMap cancels the previous request before making a new one. However, toSignal(toObservable(...)) adds boilerplate codes to the component and the component becomes unmaintainable when this pattern is repeatedly seen. When a root service creates an Observable that does not unsubscribe, then toSignal can lead to memory leaks. Engineers should use this pattern with care in root services.

Signal + computedAsync

// main.ts

import { computedAsync } from 'ngxtension/computed-async';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe, PokemonComponent],
  template: `
      <h3>Signal + computedAsync</h3>
      @if (pokemon4(); as pokemon4) {
         <app-pokemon [pokemon]="pokemon4" />
      }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  pokemonId = signal(25);
  getPokemon = getPokemonFn();

  pokemon4 = computedAsync(() => this.getPokemon(this.pokemonId()));
}
Enter fullscreen mode Exit fullscreen mode

Use ngxtension's computedAsync to retrieve the Pokemon via HttpClient.

My two cents: The utility function supports Promise and Observable, and returns a Signal<T | undefined>. The function uses Subject to emit value and performs cleanup in the callback of DestroyRef. The default behavior is switchAll which cancels the previous request. It has all the benefits of the earlier patterns and not the drawbacks. Angular engineers do not have to worry about AsyncPipe, subscriptions that require clean-up, and toSignal(toObservable(...)) boilerplate codes.

Signal + computedAsync + enable requiredSync to emit sync data

// main.ts

import { computedAsync } from 'ngxtension/computed-async';

const DEFAULT_POKEMON: DisplayPokemon = {
  id: 0,
  name: '',
  img: 'https://placehold.co/400',
};

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe, PokemonComponent],
  template: `
      <h3>Signal + computedAsync + requireSync = true</h3>
      <app-pokemon [pokemon]="pokemon5()" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  pokemonId = signal(25);
  getPokemon = getPokemonFn();

  pokemon5 = computedAsync(() => this.getPokemon(this.pokemonId())
    .pipe(startWith(DEFAULT_POKEMON)),
    {
      requireSync: true,
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

This pattern also uses ngxtension's computedAsync to retrieve the Pokemon via HttpClient. The difference is requireSync option is enabled to emit synchronous data. The signal always has a value and it is impossible to have undefined.

My two cents: By providing the initial value in the startWith RxJS operator, the return type is Signal<T> instead of Signal<T | undefined>. I don't need to use @if to test undefined before displaying the signal value in the App component.

These are all the patterns that I observe when retrieving data via signal and HttpClient. I prefer to use computedAsync utility function despite it comes from a third-party library.

My reasoning is as follows:

  • No toSignal and toObservable boilerplate codes
  • No reference to AsyncPipe and Observable
  • No extra declaration of signal. Does not make HTTP requests in effect; therefore, no cleanup of subscription in the cleanup function

The following Stackblitz repo shows the final results:

This is the end of the blog post that analyzes data retrieval patterns in Angular. I hope you like the content and continue to follow my learning experience in Angular, NestJS and other technologies.

Resources:

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