Utilisation des signaux dans une application Angular

Utilisation des signaux dans une application Angular

Loïc Boutter (lazybobcat)
Loïc Boutter (lazybobcat)

Les signaux ont fait leur apparition dans Angular. Mais c’est quoi un signal au juste ? Comment on l’utilise ? C’est ce que nous allons voir.

Au sommaire :

  1. C’est quoi un signal ?
  2. À quoi ça sert ?
  3. Comment les utiliser ?
  4. Pour résumer
  5. Liens et exemple complet

C’est quoi un signal ?

Un signal, c’est un type permettant de définir des valeurs réactives. Si vous avez utilisé RxJs, vous avez utilisé des valeurs réactive de type “Observable”. Le principe est similaire, ces types représentent des valeurs qui peuvent changer au cours du temps. On peut également calculer d’autres valeurs réactives à partir des premières et seront automatiquement mises à jour.

Alors quelle différence avec RxJs ? La mise en oeuvre des signaux est bien plus simple car le code peut être écrit de façon plus impérative qu’avec RxJs (avec lequel on écrit du code réactif dans des “pipes”). Mais attention, les signaux ne remplacent pas complètement RxJs, ils viennent d’ailleurs avec des opérateurs très pratiques pour convertir un Observable en Signal et inversement.

À quoi ça sert ?

Les applications sont nombreuses.

Les signaux peuvent servir à calculer une valeur dériviée à partir d’un ou plusieurs autres signaux. Par exemple, si un signal contient une liste de Todo, un signal dérivé pourrait compter le nombre de Todo non terminées. Ainsi, si la liste initiale change, le compteur changera automatiquement.

L’état d’un composant ou d’une fonctionnalité donnée peut être définie par un ou plusieurs signaux et dérivés. Ainsi, lorsque l’état change, toutes les parties de l’application ayant besoin d’être informées sur cet état sont notifiées.

On peut également déclencher des “effets”, des actions à réaliser à chaque fois qu’une valeur d’un signal change. Par exemple afin d’envoyer une requête HTTP à chaque fois que la valeur d’un signal change.

Comment les utiliser ?

Premièrement, il faut Angular 16+. D’autres fonctionnalités dans Angular utilisant les signaux ont été (et seront encore) ajoutées dans les versions suivantes. Je vous recommande donc d’avoir la dernière version disponible.

Dans la suite de l’article, nous allons prendre l’exemple d’un Pokédex utilisant un signal donnant les Pokémon attrapés. Un exemple complet fonctionnel est disponible sur Stackblitz.

signal()

Nous souhaitons avoir un service Pokedex possédant les Pokémons attrapés. Pour cela, nous pouvons déclarer une variable et utiliser la fonction signal() pour l’initialiser. Nous allons créer un attribut pokemons pour le service qui sera un Signal<Pokemon[]> initialisé avec un tableau vide :

pokedex.service.ts
import { Injectable, Signal, signal } from '@angular/core';
import { Pokemon } from './pokemon.model';
 
@Injectable({ providedIn: 'root' })
export class Pokedex {
    pokemons = signal<Pokemon[]>([]);
}

Nous pouvons maintenant avoir une méthode qui va définir la liste les Pokémons, appellons la setPokemons(). On peut utiliser la méthode set() d’un signal pour définir sa valeur :

pokedex.service.ts
function setPokemons(pokemons: Pokemon[]): void {
    this.pokemons.set(pokemons);
}

Lorsque le signal est mis à jour (ici avec set()), tous les composants, services ou vues qui utilisent ce signal seront également mis à jour.

On peut lire la valeur de pokemons en l’utilisant comme une fonction :

pokedex = inject(Pokedex);
pokedex.pokemons(); // <- retourne "Pokemon[]"

En plus de la méthode set(), les signaux ont également une méthode update() qui prend en paramètre une fonction de callback. Cette fonction prendra elle-même en paramètre la valeur actuelle du signal et il faut qu’elle retourne la nouvelle valeur pour le signal. C’est très pratique dans notre cas, car on ne souhaite ajouter qu’un seul Pokémon à la fois dans notre Pokédex :

pokedex.service.ts
function addPokemon(pokemon: Pokemon): void {
    // On copie la liste existante et on y ajoute le nouveau Pokémon
    this.pokemons.update(pokemons => [...pokemons, pokemon]);
}

computed()

Puisque nous avons un signal nous permettant de toujours avoir une liste de Pokémon à jour, il serait intéressant de pouvoir calculer tout un tas de valeurs dérivées qui resteraient également à jour à chaque fois que la liste change. Par exemple, on pourrait avoir une valeur contenant le nombre de Pokémons attrapés et une autre les triant dans l’ordre du Pokédex.

Pour ce faire, on peut utiliser la fonction computed(). Cette fonction créé un nouveau signal qui dépendra d’un ou plusieurs autres signaux. À chaque fois que ces signaux changeront, la valeur du signal “computed” sera recalculée.

computed() prend en paramètre une fonction :

captured = computed(() => this.pokedex.pokemons().length);
orderedPokemons = computed(() => this.pokedex.pokemons().sort(
    (a, b) => a.pokedexId < b.pokedexId ? -1 : 1)
);

Note : nous récupérons bien la valeur du signal pokemons en l’invoquant : this.pokedex.pokemons().

effect()

Un effet est une fonction qui se déclenche lorsqu’un ou plusieurs signaux changent. Il n’a pas pour but de changer l’état de l’application (ce n’est donc pas comme computed()). J’aime voir les effets comme un “effet secondaire” ou un “effet de bord” : lorsqu’un signal change il faut effectuer une certaine opération comme logguer une information, appeler une API, stocker quelque chose dans le local storage, etc.

Les effect() sont asynchrones et ne se produisent pas directement après le changement d’un signal, ils sont déclenchés lors du processus de changement de détection. Même si c’est techniquement possible, les effets ne devraient pas mettre à jour un autre signal : si vous entrez dans ce cas de figure, vous pouvez probablement utiliser computed().

Dans notre exemple, nous allons ajouter un signal contenant un certains nombre de Pokéballs :

pokeballs = signal(6);

Nous allons ensuite pouvoir créer un effet qui va dépendre de ce nombre de Pokéballs : à chaque fois que ce nombre change, nous présentons au dresseur un nouveau Pokémon qu’il pourra attraper ou non. S’il l’attrape, le nombre de Pokéball va diminuer, déclenchant à nouveau cet effet et ainsi de suite.

hautes-herbes.component.ts
@Component({
    // ...
})
export class HautesHerbesComponent {
    // ...
    pokeballs = signal(6);
 
    constructor() {
        effect(() => {
            // Comme on utilise le signal "pokeballs()", l'effet sera
            // redéclenché à chaque fois que la valeur change
            if (this.pokeballs() > 0) {
                this.generateEncounter();
            }
        });
    }
 
    function generateEncounter(): void {
        // rencontre avec un Pokémon aléatoire
    }
}

Vous remarquerez que nous avons déclaré effect() dans le constructeur. C’est parce qu’il nécessite un “contexte d’injection”, on peut donc l’utiliser :

  • dans un constructeur
  • assigné à un attribut d’une directive ou d’un service
  • en passant le service d’injection d’Angular en second paramètre de l’effet :
effect(() => {
    //...
}, {injector: this.injector});

Pour résumer

Nous avons vu qu’un signal() est une valeur réactive, changeante dans le temps. On peut modifier sa valeur avec set() ou update() et récupérer sa valeur en l’utilisant comme une fonction.

Un computed() est un signal dérivant d’un ou plusieurs autres. Il est recalculé automatiquement dès que l’un des signaux le constituant change.

Un effect() est une fonction “effet de bord” permettant d’exécuter des opérations supplémentaires lorsqu’un ou plusieurs signaux changent, comme appeler une API, sauvegarder des informations ou mettre à jour la vue.

Liens et exemple complet

Vous pouvez retrouver l’exemple complet sur Stackblitz.

Voici également quelques liens vers des sources de documentation et de quoi aller plus loin avec les signaux :

Retour aux articles