Leçon 06 · Phase 1 — Réactivité moderne Signals · ~17 min

Signals #1 : signal() & computed()

Jusqu'ici tes données étaient de simples propriétés (mesure = 21.4). Un signal, c'est une valeur qui sait quand elle change et qui prévient tout ce qui en dépend. C'est le nouveau cœur de la réactivité Angular : la vue, et les valeurs dérivées, se remettent à jour automatiquement et au plus juste. C'est le concept le plus structurant de toute la phase 1.

Pourquoi ça compte pour Alliance Un ERP, c'est des valeurs qui en dépendent d'autres : un seuil qui change → un badge passe au rouge → un compteur d'alertes s'incrémente → le titre de l'onglet se met à jour. Avec des propriétés nues, tu recalcules tout à la main (et tu oublies des cas). Avec les signals, tu déclares la dépendance une fois et Angular propage. C'est aussi ce que l'équipe écrit déjà : ne pas savoir lire un signal() / computed(), c'est ne pas pouvoir reviewer leurs composants.

1. Un signal, c'est quoi ?

Un signal est un conteneur autour d'une valeur. Tu ne lis plus la valeur directement : tu l'appelles comme une fonction. Et tu ne l'écris plus avec = : tu utilises .set() ou .update(). Ce petit protocole est ce qui permet à Angular de savoir qui lit quoi et quand ça change.

import { signal } from '@angular/core';

export class PosteComponent {
  // création : signal(valeurInitiale)
  mesure = signal(21.4);

  lire() {
    const v = this.mesure();      // LIRE = appeler ()
  }
  ecrire() {
    this.mesure.set(23.0);        // remplacer la valeur
    this.mesure.update(v => v + 1); // dériver de l'ancienne
  }
}

Dans le template, pareil : on appelle le signal. {{ mesure() }}, pas {{ mesure }}. Dès que set/update est appelé, toute vue qui lit mesure() se rafraîchit — et uniquement celles-là.

Analogie : la cellule Excel (la bonne intuition) Une cellule A1 = 21.4 et une cellule B1 = A1 * 2 : tu changes A1, B1 se recalcule tout seul, sans que tu touches B1. Un signal, c'est A1 ; un computed() (section 3), c'est B1. Côté .NET, c'est l'idée d'INotifyPropertyChanged — mais sans écrire l'événement ni le PropertyChanged?.Invoke à la main : la notification est automatique et le système sait exactement qui dépend de qui.

2. set vs update

Deux façons d'écrire dans un signal :

MéthodeSert àExemple
.set(v)remplacer par une valeur connueseuil.set(80)
.update(fn)calculer à partir de l'anciennecompteur.update(n => n + 1)

Règle simple : si la nouvelle valeur dépend de l'ancienne (incrément, toggle, ajout dans un tableau), c'est update. Sinon set.

3. computed() — une valeur dérivée

Un computed est un signal en lecture seule dont la valeur est calculée à partir d'autres signals. Tu ne l'écris jamais : il se recalcule quand l'un de ses ingrédients change.

import { signal, computed } from '@angular/core';

export class PosteComponent {
  mesure = signal(21.4);
  seuil  = signal(25);

  // dérivé : recalculé quand mesure() OU seuil() change
  enAlerte = computed(() => this.mesure() > this.seuil());

  libelle = computed(() =>
    this.enAlerte() ? '⚠ Hors seuil' : 'OK'
  );
}

Angular détecte automatiquement les signals lus dans la fonction (mesure et seuil) : ce sont ses dépendances. Tu ne les déclares pas, tu les utilises, et le lien se crée tout seul. Note que libelle dépend de enAlerte qui dépend de mesure/seuil : la chaîne se propage.

Deux propriétés du computed à connaître (1) Mémoïsé : tant que ses dépendances ne changent pas, il renvoie la valeur en cache sans recalculer — même si tu le lis 100 fois. (2) Paresseux : il ne calcule que si quelqu'un le lit. C'est exactement ce qui règle le piège perf de la leçon 04 : au lieu de {{ calculLourd() }} (rejoué à chaque cycle), tu écris calcul = computed(() => …) et {{ calcul() }} — recalculé seulement quand une vraie dépendance bouge.

4. Dans le template

<!-- on appelle chaque signal / computed avec () -->
<p>Mesure : {{ mesure() }} °C</p>
<span [class.ko]="enAlerte()">{{ libelle() }}</span>
<button (click)="seuil.set(30)">Relever le seuil</button>

Clique sur le bouton → seuil change → enAlerte et libelle se recalculent → les deux liaisons se rafraîchissent. Tu n'as écrit aucune logique de mise à jour. C'est tout l'intérêt.

Œil de tech lead (1) Une valeur dérivée d'autres signals = computed, jamais une méthode appelée dans le template (qui, elle, est rejouée à chaque détection). C'est le réflexe perf clé. (2) Un computed doit être pur : il calcule et renvoie, il ne modifie rien (pas de .set() d'un autre signal dedans, pas d'appel HTTP). Les effets de bord, ce sera effect() — prochaine leçon. (3) En revue, repère le {{ mesure }} sans parenthèses : ça affiche la fonction, pas la valeur — bug silencieux classique du débutant signals.

5. Vérifie ta compréhension

Comment lit-on la valeur d'un signal() nommé mesure ?
Tu veux une valeur enAlerte = mesure > seuil, recalculée toute seule. Tu écris ?
Pour incrémenter un signal compteur à partir de sa valeur actuelle ?
À retenir Un signal(v) est une valeur réactive : on la lit en l'appelant (mesure()) et on l'écrit avec .set(v) (remplacer) ou .update(fn) (dériver de l'ancienne). Un computed(() => …) est une valeur dérivée en lecture seule : Angular détecte seul ses dépendances, et il est mémoïsé + paresseux. Dans le template, on appelle toujours avec (). Réflexe de lead : valeur dérivée → computed (jamais une méthode dans le template), et un computed reste pur.
Source primaire à lire (10 min)

Angular — Signals (officiel). Lis l'intro + les sections Writable signals et Computed signals. On verra effect() et les input()/output() signals à la leçon 07.

Je suis ton prof — pose-moi des questions. Tu veux qu'on transforme le tableau de postes de la leçon 05 en version signals (mesure + seuil + badge dérivé), ou que je t'explique pourquoi un computed est « paresseux » et ce que ça change concrètement pour la perf ? Demande-moi, ici, maintenant.
◀ 05 · Control flow @if / @for / @switch Suivant : 07 · Signals #2 : effect(), input() / output() ▶