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.