Angular : comprendre simplement les Signals avec signal(), computed(), effect() et model()

Les Signals occupent aujourd’hui une place importante dans Angular. Ils permettent de gérer plus simplement les données qui changent dans une application.

Si vous débutez avec Angular, des termes comme signal(), computed(), effect() ou model() peuvent sembler compliqués au premier abord.

Dans cet article, nous allons les découvrir pas à pas, à travers un exemple très simple : un petit panier d’achat.

À la fin de cet article, vous saurez utiliser :

  • signal() pour stocker une donnée ;
  • computed() pour calculer automatiquement une valeur ;

  • effect() pour réagir à un changement ;

  • model() pour partager et modifier une donnée entre un composant parent et un composant enfant.

Qu’est-ce qu’un Signal dans Angular ?

Un Signal est une valeur qu’Angular peut surveiller.

Prenons un exemple très simple :

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

prix = signal(25);

Ici, nous créons un Signal appelé prix.

Sa valeur actuelle est :

25

Pour récupérer la valeur d’un Signal, nous utilisons des parenthèses :

console.log(this.prix());

Le résultat sera :

25

Il faut donc bien retenir cette différence :

this.prix

représente le Signal lui-même, tandis que :

this.prix()

permet de récupérer la valeur contenue dans le Signal.

Notre projet : un petit panier d’achat

Pour comprendre les Signals, nous allons créer une petite application.

Elle affichera :

Mon panier

Prix : 25 €

Quantité : 1

[-] 1 [+]

Total : 25 €

Lorsque l’utilisateur clique sur +, la quantité augmente :

Quantité : 2

et le total est automatiquement recalculé :

Total : 50 €

Nous allons ensuite déplacer les boutons de quantité dans un composant enfant pour découvrir model().

Créer le projet Angular

Commençons par créer notre application :

ng new exercice-signals

Entrez ensuite dans le projet :

cd exercice-signals

Puis démarrez Angular :

ng serve

Votre application est maintenant prête.

Créer notre premier Signal avec signal()

Ouvrons :

app.component.ts

Nous allons importer signal :

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

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {

  // Prix de notre produit.
  // La valeur initiale est 25.
  prix = signal(25);

  // Quantité sélectionnée.
  // Au démarrage, nous voulons une quantité de 1.
  quantite = signal(1);

}

Nous avons maintenant deux Signals :

prix = signal(25);

quantite = signal(1);

Le premier contient le prix.

Le deuxième contient la quantité.

Afficher un Signal dans le HTML

Dans :

app.component.html

nous pouvons écrire :

<h1>Mon panier</h1>

<p>
  Prix :
  <strong>{{ prix() }} €</strong>
</p>

<p>
  Quantité :
  <strong>{{ quantite() }}</strong>
</p>

Vous remarquerez que nous écrivons :

{{ prix() }}

et non :

{{ prix }}

C’est exactement le même principe que dans notre fichier TypeScript.

Pour lire la valeur d’un Signal, nous utilisons :

()

Modifier un Signal

Nous allons maintenant permettre à l’utilisateur d’augmenter ou de diminuer la quantité.

Angular nous donne notamment deux méthodes très utiles :

set()
update()

Utiliser set()

set() permet de remplacer directement la valeur d’un Signal.

Par exemple :

this.quantite.set(5);

La quantité devient alors :

5

Nous pouvons également faire :

this.quantite.set(
  this.quantite() + 1
);

Mais pour ce genre de modification, update() est souvent plus pratique.

Utiliser update()

Ajoutons deux méthodes dans notre composant :

augmenter() {

  // q représente la quantité actuelle.
  // Nous retournons la nouvelle quantité.
  this.quantite.update(q => q + 1);

}

diminuer() {

  // Nous vérifions que la quantité
  // est supérieure à 1.
  if (this.quantite() > 1) {

    // Nous retirons 1.
    this.quantite.update(q => q - 1);

  }

}

Avec :

this.quantite.update(q => q + 1);

Angular récupère automatiquement la valeur actuelle.

Si la quantité est :

1

Angular fait :

1 + 1

La nouvelle valeur devient :

2

Ajouter les boutons

Dans app.component.html, ajoutons :

<h1>Mon panier</h1>

<p>
  Prix :
  <strong>{{ prix() }} €</strong>
</p>

<p>
  Quantité :
  <strong>{{ quantite() }}</strong>
</p>

<button (click)="diminuer()">
  -
</button>

<button (click)="augmenter()">
  +
</button>

Lorsque vous cliquez sur +, le Signal change.

Par exemple :

1 → 2 → 3 → 4

Nous savons maintenant :

  • créer un Signal ;
  • lire sa valeur ;

  • modifier sa valeur.

Nous pouvons passer à computed().

Qu’est-ce que computed() ?

Notre panier possède actuellement :

Prix : 25 €
Quantité : 3

Nous voulons calculer :

25 × 3 = 75 €

Nous pourrions faire ce calcul manuellement.

Mais Angular propose une solution beaucoup plus intéressante :

computed()

Un computed() représente une valeur calculée à partir d’autres Signals.

Importons-le :

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

Puis ajoutons :

total = computed(() => {

  return this.prix() * this.quantite();

});

Notre composant contient maintenant :

prix = signal(25);

quantite = signal(1);

total = computed(() => {
  return this.prix() * this.quantite();
});

Pourquoi computed() est-il intéressant ?

Imaginons :

Prix : 25 €
Quantité : 1

Le total est :

25 €

Nous cliquons sur +.

La quantité devient :

2

Angular sait que notre computed() utilise :

this.quantite()

Le total sera donc recalculé automatiquement lorsque sa valeur sera nécessaire.

Nous obtenons :

25 × 2 = 50 €

Si la quantité passe à :

3

nous obtenons :

25 × 3 = 75 €

Nous n’avons pas besoin de recalculer le total manuellement après chaque clic.

C’est tout l’intérêt de computed().

Afficher le total

Ajoutons maintenant le total dans notre HTML :

<h1>Mon panier</h1>

<p>
  Prix :
  <strong>{{ prix() }} €</strong>
</p>

<p>
  Quantité :
  <strong>{{ quantite() }}</strong>
</p>

<button (click)="diminuer()">
  -
</button>

<button (click)="augmenter()">
  +
</button>

<hr>

<h2>
  Total : {{ total() }} €
</h2>

Nous pouvons déjà observer une petite chaîne réactive :

quantite
   ↓
signal()
   ↓
computed()
   ↓
total

Lorsque la quantité change, notre total s’adapte automatiquement.

Découvrir effect()

effect() ne sert pas à calculer une nouvelle valeur.

Il sert à exécuter une action lorsqu’une donnée réactive utilisée par cet effet change.

Nous pouvons, par exemple, afficher le total dans la console.

Importons effect :

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

Puis :

constructor() {

  effect(() => {

    console.log(
      'Nouveau total :',
      this.total(),
      '€'
    );

  });

}

Lorsque le total change, notre effet sera exécuté.

Si nous cliquons plusieurs fois sur +, nous pourrons obtenir dans la console :

Nouveau total : 25 €
Nouveau total : 50 €
Nouveau total : 75 €
Nouveau total : 100 €

Comment effect() sait-il quoi surveiller ?

Regardons cette ligne :

this.total()

Elle se trouve à l’intérieur de notre effect() :

effect(() => {

  console.log(this.total());

});

Angular détecte les dépendances réactives lues pendant l’exécution de l’effet.

Ici, notre effet dépend de :

total()

Et total() dépend lui-même de :

prix()

et :

quantite()

computed() ou effect() ?

Les débutants peuvent facilement confondre les deux.

La différence principale est simple.

Utilisez computed() lorsque vous voulez calculer une valeur.

Par exemple :

total = computed(() => {
  return this.prix() * this.quantite();
});

Utilisez effect() lorsque vous voulez déclencher une action liée à un changement réactif.

Par exemple :

effect(() => {
  console.log(this.total());
});

Découvrir model()

Nous allons maintenant découvrir une autre fonctionnalité intéressante :

model()

Pour comprendre son utilité, nous allons légèrement améliorer notre application.

Nous allons créer un composant enfant responsable de la quantité.

Le composant enfant pourra afficher et modifier la quantité.

Lorsque l’enfant modifie la quantité, le parent récupère également la nouvelle valeur.

C’est ce que nous allons faire avec model().

Créer le composant enfant

quantity/
├── quantity.ts
├── quantity.html
└── quantity.css

Utiliser model() dans le composant enfant

Ouvrons : 

quantity.ts

et écrivons :

import {
  Component,
  model
} from '@angular/core';

@Component({
  selector: 'app-quantity',
  standalone: true,
  imports: [],
  templateUrl: './quantity.html',
  styleUrl: './quantity.css'
})
export class Quantity {

  // model() permet ici de recevoir une valeur
  // depuis le composant parent,
  // mais également de la modifier.
  //
  // La valeur par défaut est 1.
  quantity = model(1);


  augmenter() {

    // On récupère automatiquement
    // la valeur actuelle et on ajoute 1.
    this.quantity.update(q => q + 1);

  }


  diminuer() {

    // Nous empêchons la quantité
    // de descendre sous 1.
    if (this.quantity() > 1) {

      this.quantity.update(q => q - 1);

    }

  }

}

Notre model() ressemble beaucoup à un Signal :

quantity = model(1);

Nous pouvons lire sa valeur :

this.quantity();

Nous pouvons la modifier :

this.quantity.set(5);

ou : 

this.quantity.update(q => q + 1);

Mais model() apporte en plus un mécanisme très pratique pour la communication bidirectionnelle avec le composant parent.

Créer le HTML du composant enfant

Dans : 

quantity.html

ajoutons :

<div>

  <button (click)="diminuer()">
    -
  </button>

  <strong>
    {{ quantity() }}
  </strong>

  <button (click)="augmenter()">
    +
  </button>

</div>

Notre composant peut maintenant afficher et modifier sa quantité.

Importer le composant enfant

Retournons dans :

app.ts

Importons notre composant :

import { Quantity }
  from './quantity/quantity';

Puis ajoutons-le dans imports :

@Component({
  selector: 'app-root',
  standalone: true,

  imports: [
    QuantityComponent
  ],

  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})

Notre composant parent connaît maintenant Quantity.

Utiliser model() avec le parent

Voici la partie la plus intéressante.

Dans : 

app.html

nous pouvons écrire :

<app-quantity
  [(quantity)]="quantite">
</app-quantity>

Regardez bien cette syntaxe :

[(quantity)]="quantite"

Les caractères :

[()]

indiquent une liaison dans les deux directions.

Le parent fournit donc la quantité à l’enfant.

Mais lorsque l’enfant modifie la quantité, la nouvelle valeur est également communiquée au parent.

C’est une liaison bidirectionnelle.

Notre composant parent complet

Voici maintenant notre app.ts :

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

import { Quantity }
  from './quantity/quantity';

@Component({
  selector: 'app-root',
  standalone: true,

  imports: [
    Quantity
  ],

  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {

  // Prix du produit.
  prix = signal(25);


  // Quantité sélectionnée.
  quantite = signal(1);


  // Le total dépend du prix
  // et de la quantité.
  total = computed(() => {

    return this.prix() * this.quantite();

  });


  constructor() {

    // Cet effet utilise total().
    //
    // Lorsque ses dépendances réactives changent
    // et que le total évolue, l'effet pourra
    // être exécuté à nouveau.
    effect(() => {

      console.log(
        'Nouveau total :',
        this.total(),
        '€'
      );

    });

  }
}

Le HTML final

Notre app.html devient :

<h1>
  Mon panier Angular 
</h1>

<p>
  Prix :
  <strong>
    {{ prix() }} €
  </strong>
</p>


<p>
  Choisissez la quantité :
</p>

<app-quantity
  [(quantity)]="quantite">
</app-quantity>


<hr>


<p>
  Quantité sélectionnée :
  <strong>
    {{ quantite() }}
  </strong>
</p>


<h2>
  Total :
  {{ total() }} €
</h2>

Que se passe-t-il lorsque nous cliquons sur + ?

C’est ici que nous pouvons réellement comprendre l’intérêt des Signals.

Au départ :

Prix : 25 €
Quantité : 1
Total : 25 €

Nous cliquons sur :

+

Le composant enfant exécute :

this.quantity.update(q => q + 1);

La quantité devient :

2

Grâce à model(), la nouvelle valeur est communiquée au parent.

Le Signal :

quantite

du parent contient maintenant :

2

Notre computed() utilise justement cette quantité :

total = computed(() => {
  return this.prix() * this.quantite();
});

Le total devient donc :

25 × 2 = 50 €

Et notre effect() utilise :

this.total()

Il peut donc afficher :

Nouveau total : 50 €

dans la console.

Conclusion

signal()
→ Je stocke une donnée qui peut changer.

computed()
→ Je calcule une valeur à partir d'autres Signals.

effect()
→ Je déclenche une action lorsqu'un état réactif utilisé change.

model()
→ Je facilite une valeur modifiable entre un parent et un enfant.

Cet exemple est volontairement simple. Le but n’est pas seulement de créer un panier, mais surtout de comprendre comment Angular peut suivre les changements de données et mettre à jour automatiquement les parties de l’application qui en dépendent.

Une fois ces bases comprises, vous pourrez utiliser les Signals dans des projets plus complets : formulaires, authentification, listes d’articles, paniers d’achat, appels API, services Angular ou encore gestion de l’état d’une application.

Le plus important est de commencer simplement : créez quelques Signals, modifiez-les, observez les valeurs calculées avec computed(), puis ajoutez progressivement effect() et model().

C’est en pratiquant que le fonctionnement des Signals devient naturel.