How to Leverage Signals in Angular Services for Better Data Communication
Tags: angular, tutorial, beginners, signals
Published: 2023-06-18 04:15:38

##Introduction

In this blog post, I would like to convert "Service with a Subject" to "Service with a Signal " and expose signals only.  It is made possible by calling `toSignal` to convert Observable to signal.  Then, I can pass signal values to Angular components to display data.   After using signal values directly in the application, inline templates don't have to use async pipe to resolve Observable. Moreover, imports array of the components do not need to `NgIf` and `AsyncPipe`. 

###Source codes of "Service with a Subject"

```typescript
// pokemon.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class PokemonService {
  private readonly pokemonIdSub = new Subject<number>();
  readonly pokemonId$ = this.pokemonIdSub.asObservable();

  updatePokemonId(pokemonId: number) {
    this.pokemonIdSub.next(pokemonId);
  }
}
```

```typescript
// pokemon.http.ts

export 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,
        abilities: pokemon.abilities.map((ability) => ({
          name: ability.ability.name,
          is_hidden: ability.is_hidden
        })),
        stats: pokemon.stats.map((stat) => ({
          name: stat.stat.name,
          effort: stat.effort,
          base_stat: stat.base_stat,
        })),
      }))
    );
}

export const getPokemonId = () => inject(PokemonService).pokemonId$;
```

```typescript
// pokemon.component.ts

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [AsyncPipe, NgIf, PokemonControlsComponent, PokemonAbilitiesComponent, PokemonStatsComponent, PokemonPersonalComponent],
  template: `
    <h1>
      Display the first 100 pokemon images
    </h1>
    <div>
      <ng-container *ngIf="pokemon$ | async as pokemon">
        <div class="container">
          <img [src]="pokemon.front_shiny" />
          <img [src]="pokemon.back_shiny" />
        </div>
        <app-pokemon-personal [pokemon]="pokemon"></app-pokemon-personal>
        <app-pokemon-stats [stats]="pokemon.stats"></app-pokemon-stats>
        <app-pokemon-abilities [abilities]="pokemon.abilities"></app-pokemon-abilities>
      </ng-container>
    </div>
    <app-pokemon-controls></app-pokemon-controls>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  retrievePokemon = retrievePokemonFn();
  pokemon$ = getPokemonId().pipe(switchMap((id) => this.retrievePokemon(id)));
}
```

`PokemonService` encapsulates `pokemonIdSub` subject and exposes `pokemonId$` Observable.  In `PokemonComponent`, I invoke `retrievePokemon` function to retrieve a new Pokemon whenever `pokemonId$` emits a new id. `pokemon$` is a Pokemon Observable that I resolve in the inline template in order to assign the Pokemon object to child components.

Next, I am going to convert `PokemonService` from "Service with a Subject" to "Service with a Signal" to highlight the benefits of using signals.

###Conversion to "Service with a Signal"

First, I combine `pokemon.http.ts` and `pokemon.service.ts` to move `retrievePokemonFn` to the service.

```typescript
// pokemon.service.ts

// Point 1: move helper functions to this service
const retrievePokemonFn = () => {
  const httpClient = inject(HttpClient);
  return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`);
}

const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => {
  const stats = pokemon.stats.map((stat) => ({
    name: stat.stat.name,
    effort: stat.effort,
    baseStat: stat.base_stat,
  }));

  const abilities = pokemon.abilities.map((ability) => ({
    name: ability.ability.name,
    isHidden: ability.is_hidden
  }));

  const { id, name, height, weight, sprites } = pokemon;
  
  return {
    id,
    name,
    height,
    weight,
    backShiny: sprites.back_shiny,
    frontShiny: sprites.front_shiny,
    abilities,
    stats,
  }
}

@Injectable({
  providedIn: 'root'
})
export class PokemonService {
  private readonly pokemonId = signal(1);
  private readonly retrievePokemon = retrievePokemonFn()

  pokemon$ = toObservable(this.pokemonId).pipe(
    switchMap((id) => this.retrievePokemon(id)),
    map((pokemon) => pokemonTransformer(pokemon)),
  );

  updatePokemonId(input: PokemonDelta | number) {
    if (typeof input === 'number') {
      this.pokemonId.set(input);
    } else {
      this.pokemonId.update((value) => {
        const newId = value + input.delta;
        return Math.min(input.max, Math.max(input.min, newId));
      });
    }
  }
}
```

`pokemonId` is a signal that stores Pokemon id. When `toObservable(this.pokemonId)` emits an id, the Observable invokes `this.retrievePokemon` to retrieve a Pokemon and `pokemonTransformer` to transform the data. .

Next, I am going to modify components to use signals instead of Observable.

###Modify Pokemon Component to use signals

```typescript
const initialValue: DisplayPokemon = {
  id: -1,
  name: '',
  height: 0,
  weight: 0,
  backShiny: '',
  frontShiny: '',
  abilities: [],
  stats: [],
}

export class PokemonComponent {
  service = inject(PokemonService);

  // Point 2:  convert Observable to signal using toSignal
  pokemon = toSignal(this.pokemonService.pokemon$, { initialValue });
  
  // Point 3: compute a signal from an existing signal
  personalData = 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 },
    ];
  });
}
```

`PokemonComponent` injects `PokemonService` to access `pokemon$` Observable. Then, `toSignal` converts the Pokemon Observable to a Pokemon signal.

`personalData` is a computed signal that derives from `this.pokemon()` signal value. It is a signal that returns the id, name, height and weight of a Pokemon

Without the `pokemon$` Observable, I revise the inline template to render signal values and pass signal values to children components.

```typescript
@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [PokemonControlsComponent, PokemonAbilitiesComponent, PokemonStatsComponent, PokemonPersonalComponent],
  template: `
    <h2>
      Display the first 100 pokemon images
    </h2>
    <div>
      <ng-container>
        <div class="container">
          <img [src]="pokemon().frontShiny" />
          <img [src]="pokemon().backShiny" />
        </div>
        <app-pokemon-personal [personalData]="personalData()" />
        <app-pokemon-stats [stats]="pokemon().stats" />
        <app-pokemon-abilities [abilities]="pokemon().abilities" />
      </ng-container>
    </div>
    <app-pokemon-controls />
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent { ... }
```

One obvious change is the inline template eliminates ngContainer, ngIf and async pipe.  It also leads to the removal of `AsyncPipe` and `NgIf` from the imports array.  

The inline template invokes `pokemon()` multiple times to access frontShiny, backShiny, stats and abilities properties. stats and abilities subsequently become the inputs of `PokemonStatsComponent` and `PokemonAbilitiesComponent` respectively. 

Similarly, the result of `personalData()` is passed to `personalData` input of `PokemonPersonalComponent`.  

###Modify child components to accept signal value input

The application breaks after code changes in `PokemonComponent`.  It is because the input of `PokemonPersonalComponent` has different type.  In order to fix the problem,  I correct the input value of the child component.

```typescript
// pokemon-personal.component.ts

@Component({
  selector: 'app-pokemon-personal',
  standalone: true,
  imports: [NgTemplateOutlet, NgFor],
  template:`
    <div class="pokemon-container" style="padding: 0.5rem;">
      <ng-container *ngTemplateOutlet="details; context: { $implicit: personalData }"></ng-container>
    </div>
    <ng-template #details let-personalData>
      <label *ngFor="let data of personalData">
        <span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
        <span>{{ data.value }}</span>
      </label>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonPersonalComponent {
  @Input({ required: true })
  personalData: ({ text: string; value: string; } | { text: string; value: number })[];
}
```

I replace `pokemon` input with `personalData` and use the latter in the inline template to render array values.  

If I use Observable in `PokemonComponent`, I cannot construct `personalData` in a reactive manner.   I would subscribe Pokemon Observable and construct `personaData` in the callback.  Furthermore, I complete the Observable using `takeUntilDestroyed` to prevent memory leak.  

This is it and I have converted the Pokemon service from "Service with a Subject" to "Service with a Signal".  The Pokemon service encapsulates HTTP call, converts Observable to signal and exposes signals to outside. In components, I call signal functions within inline templates to display their values.  Moreover, the components stop importing `NgIf` and `AsyncPipe` because they don't need to resolve Observable.

{% embed https://stackblitz.com/edit/angular-dkbeeh?file=src%2Fpokemon%2Fservices%2Fpokemon.service.ts %}

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:

- Github Repo: https://github.com/railsstudent/ng-pokemon-signal/blob/main/projects/pokemon-signal-demo-5
- Stackblitz: https://stackblitz.com/edit/angular-dkbeeh?file=src%2Fpokemon%2Fservices%2Fpokemon.service.ts
- PokeAPI: https://pokeapi.co/