À propos de ce cours
Un signal est une valeur qui prévient automatiquement ceux qui la lisent quand elle change. C’est grâce à eux qu’Angular 22, sans zone.js et avec OnPush par défaut, met à jour exactement ce qui doit l’être. Ce chapitre présente tous les outils :
signal(), set(), update(), asReadonly() et l’option equal, avec la règle d’or des objets et des tableaux : toujours créer une nouvelle valeur ;
computed() : des valeurs dérivées mémorisées, paresseuses et en lecture seule ;
effect() : les actions vers le monde extérieur (stockage, titre, minuteur), avec onCleanup et untracked, et les cas où il ne faut pas l’utiliser ;
linkedSignal() : une valeur dérivée que l’utilisateur peut modifier ;
resource() : des données asynchrones exposées en signals (value, status, isLoading, error, reload) ;
toSignal() et toObservable() : le pont avec RxJS ;
les pièges les plus courants et le choix du bon outil.
-
Objectifs
- Comprendre ce qu'est un signal et pourquoi Angular s'appuie sur eux pour mettre l'affichage à jour
- Créer, lire et modifier un signal avec
signal(),set()etupdate(), sans modifier les objets sur place - Calculer des valeurs dérivées avec
computed(), et des valeurs dérivées modifiables aveclinkedSignal() - Déclencher des actions avec
effect(), et savoir quand ne pas l'utiliser - Charger des données asynchrones avec
resource()et suivre leur état - Faire le lien avec RxJS grâce à
toSignal()ettoObservable()
-
Présentation
- Un signal est une boîte qui contient une valeur et qui prévient automatiquement tous ceux qui l'ont lue lorsque cette valeur change. Le template, un
computedou uneffectqui lisent un signal en deviennent les consommateurs. - C'est sur ce mécanisme que repose l'Angular moderne : depuis Angular 21, les nouveaux projets n'utilisent plus
zone.js, et Angular 22 appliqueOnPushpar défaut. Angular ne met donc à jour un composant que lorsqu'un signal qu'il affiche a changé (voir « Le data binding dans Angular »). - Vous utilisez déjà des signals depuis le début de ce cours :
input(),model(),viewChild()et les Signal Forms en créent tous. Ce chapitre présente l'ensemble des outils et la manière de bien les combiner.
- Un signal est une boîte qui contient une valeur et qui prévient automatiquement tous ceux qui l'ont lue lorsque cette valeur change. Le template, un


- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/signals/. Tous les exemples ont été testés avec Angular 22.
-
Créer, lire et modifier un signal
signal(valeurInitiale)crée un signal modifiable (typeWritableSignal<T>). On le lit en l'appelant:compteur(). On le modifie avecset(nouvelleValeur), ou avecupdate(ancienne => nouvelle)quand la nouvelle valeur dépend de l'ancienne.- Un signal ne prévient ses consommateurs que s'il reçoit une valeur différente. Par défaut, il compare avec
Object.is; l'optionequalpermet de définir sa propre règle.
import { Component, Signal, WritableSignal, signal } from '@angular/core';
interface Point {
x: number;
y: number;
}
@Component({
selector: 'app-bases',
template: `
<p>Compteur : {{ compteur() }}</p>
<button (click)="compteur.set(0)">Remettre à zéro</button>
<button (click)="compteur.update((n) => n + 1)">+1</button>
<p>Inscrits : {{ inscrits().join(', ') }} ({{ inscrits().length }})</p>
<button (click)="inscrire('Sami')">Inscrire Sami</button>
<p>Position : {{ position().x }}, {{ position().y }} – changements : {{ changements }}</p>
<button (click)="deplacer(0, 0)">Même position</button>
<button (click)="deplacer(10, 5)">Déplacer</button>
`,
})
export class Bases {
// Un signal modifiable : sa valeur se lit en l'appelant, se modifie avec set() ou update()
protected readonly compteur: WritableSignal<number> = signal(0);
protected readonly inscrits = signal<string[]>(['Amira']);
// equal : fonction qui décide si la nouvelle valeur est « la même » que l'ancienne
protected readonly position = signal<Point>({ x: 0, y: 0 }, { equal: (a, b) => a.x === b.x && a.y === b.y });
protected changements = 0;
// Version en lecture seule, à exposer aux autres classes
readonly compteurLecture: Signal<number> = this.compteur.asReadonly();
protected inscrire(nom: string): void {
this.inscrits.update((liste) => [...liste, nom]); // nouveau tableau
}
protected deplacer(x: number, y: number): void {
const avant = this.position();
this.position.set({ x, y });
if (this.position() !== avant) this.changements++;
}
}
Compteur : 0
Inscrits : Amira (1)
Position : 0, 0 – changements : 0
(après +1, +1, Inscrire Sami, Même position, Déplacer, Déplacer)
Compteur : 2
Inscrits : Amira, Sami (2)
Position : 10, 5 – changements : 1
inscrits.update((liste) => [...liste, nom])crée un nouveau tableau. C'est indispensable : un signal ne voit pas qu'on modifie l'intérieur d'un tableau ou d'un objet (voir « Les pièges »).- Grâce à
equal, « Même position » et le second « Déplacer » sont ignorés : la position n'a pas vraiment changé, le signal garde son ancien objet et ne prévient personne. Seul le premier déplacement compte. asReadonly()renvoie une vue en lecture seule (typeSignal<T>) : on l'expose aux autres classes pour qu'elles puissent lire sans modifier. Un service garde ainsi le contrôle de ses données.- Attention : sans parenthèses, on manipule le signal lui-même, pas sa valeur.
this.compteur + 1est refusé :TS2365: Operator '+' cannot be applied to types 'WritableSignal<number>' and 'number'.
-
computed(): les valeurs dérivéescomputed(() => …)crée un signal en lecture seule dont la valeur est calculée à partir d'autres signals. Angular retient automatiquement les signals lus pendant le calcul : ce sont ses dépendances.- Un
computedest mémorisé (le résultat est réutilisé tant que les dépendances ne changent pas) et paresseux (il n'est calculé que lorsque quelqu'un le lit).
import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-calculs',
imports: [FormsModule],
template: `
<label>Prix <input type="number" [(ngModel)]="prix" /></label>
<label>Quantité <input type="number" [(ngModel)]="quantite" /></label>
<label>Code promo <input [(ngModel)]="code" /></label>
<p>Sous-total : {{ sousTotal() }} DT – à payer : {{ aPayer() }} DT – (sous-total relu : {{ sousTotal() }} DT)</p>
<p>{{ message() }}</p>
`,
})
export class Calculs {
protected readonly prix = signal(120);
protected readonly quantite = signal(2);
protected readonly code = signal('');
protected readonly sousTotal = computed(() => {
console.log('calcul du sous-total');
return this.prix() * this.quantite();
});
// Dépendances dynamiques : sousTotal() n'est lu que si le code est valide
protected readonly aPayer = computed(() => {
console.log('calcul du montant à payer');
return this.code() === 'ANGULAR22' ? this.sousTotal() * 0.8 : this.prix() * this.quantite();
});
protected readonly message = computed(() =>
this.code() === 'ANGULAR22' ? 'Code ANGULAR22 appliqué : −20 %' : 'Aucune réduction',
);
}
Sous-total : 240 DT – à payer : 240 DT – (sous-total relu : 240 DT)
(quantité → 3) console : calcul du sous-total
calcul du montant à payer
Sous-total : 360 DT – à payer : 360 DT – (sous-total relu : 360 DT)
(code → ANGULAR22) console : calcul du montant à payer
Sous-total : 360 DT – à payer : 288 DT – Code ANGULAR22 appliqué : −20 %

- Le sous-total est affiché deux fois, mais la console n'affiche qu'un « calcul du sous-total » par changement : la seconde lecture réutilise le résultat mémorisé.
- Quand seul le code promo change, le sous-total n'est pas recalculé : ses dépendances (
prix,quantite) n'ont pas bougé. - Les dépendances sont dynamiques: tant que le code est invalide,
aPayerne lit passousTotal()et n'en dépend donc pas. - Règle : un
computeddoit être une fonction pure: il calcule, il ne modifie rien. Écrire dans un signal depuis uncomputedprovoque l'erreurNG0600: Writing to signals is not allowed in a `computed`., et il n'a pas de méthodeset()(TS2339: Property 'set' does not exist on type 'Signal<number>'.).
-
effect(): réagir par une actioneffect(() => …)exécute une fonction une première fois, puis à chaque changement des signals qu'elle a lus. Il sert à synchroniser les signals avec le monde extérieur : stockage local, titre de la page, journal, minuteur, bibliothèque externe.- Un effet se crée dans un contexte d'injection: dans le constructeur ou l'initialisation d'une propriété. Il est arrêté automatiquement à la destruction du composant.
import { Component, effect, signal, untracked } from '@angular/core';
@Component({
selector: 'app-effets',
template: `
<p>Thème : {{ theme() }} – notifications : {{ nonLues() }}</p>
<button (click)="theme.set(theme() === 'clair' ? 'sombre' : 'clair')">Changer de thème</button>
<button (click)="nonLues.update((n) => n + 1)">Nouveau message</button>
<button (click)="auteur.set('Karim')">Changer d'auteur</button>
<p>Rappel dans : {{ rappel() }} s</p>
`,
})
export class Effets {
protected readonly theme = signal<'clair' | 'sombre'>(this.lireTheme());
protected readonly nonLues = signal(0);
protected readonly auteur = signal('Amira');
protected readonly rappel = signal(3);
constructor() {
// 1. Synchroniser une valeur avec le navigateur (stockage local)
effect(() => {
localStorage.setItem('theme', this.theme());
console.log(`thème enregistré : ${this.theme()}`);
});
// 2. Mettre à jour le titre de l'onglet ; auteur() est lu sans créer de dépendance
effect(() => {
const n = this.nonLues();
document.title = n ? `(${n}) Messages` : 'Messages';
console.log(`titre : ${document.title} – dernier auteur : ${untracked(this.auteur)}`);
});
// 3. Un minuteur relancé à chaque changement de thème, arrêté par onCleanup
effect((onCleanup) => {
this.theme();
this.rappel.set(3);
const id = setInterval(() => this.rappel.update((s) => Math.max(0, s - 1)), 1000);
onCleanup(() => clearInterval(id));
});
}
private lireTheme(): 'clair' | 'sombre' {
try {
return localStorage.getItem('theme') === 'sombre' ? 'sombre' : 'clair';
} catch {
return 'clair';
}
}
}
(au chargement) thème enregistré : clair
titre : Messages – dernier auteur : Amira
(2 × Nouveau message) titre : (1) Messages – dernier auteur : Amira
titre : (2) Messages – dernier auteur : Amira ← onglet « (2) Messages »
(Changer d'auteur) (rien : auteur est lu avec untracked)
(Changer de thème) thème enregistré : sombre ← le rappel repart de 3 s

untracked(this.auteur)lit un signal sans en faire une dépendance : changer l'auteur ne relance pas l'effet du titre.onCleanup(fn)enregistre une fonction de nettoyage, appelée avant la prochaine exécution et à la destruction : le minuteur précédent est arrêté avant d'en créer un nouveau.- Les effets sont planifiés: ils s'exécutent un peu après le changement, pendant la mise à jour de l'affichage, et une seule fois même si plusieurs signals ont changé entre-temps.
- Attention : créer un effet dans une méthode provoque
NG0203: effect() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. - À éviter : utiliser un effet pour copier un signal dans un autre. Pour calculer, utilisez
computed(); pour une valeur calculée mais modifiable,linkedSignal().
-
linkedSignal(): dérivé, mais modifiable- Certaines valeurs dépendent d'une autre et doivent pouvoir être changées par l'utilisateur : la formation sélectionnée dépend de la catégorie, mais l'utilisateur peut en choisir une autre.
linkedSignalest fait pour cela : il se recalcule quand sa source change, et accepteset()entre-temps. - Sa forme longue reçoit la valeur précédente: on peut ainsi conserver un choix de l'utilisateur lorsque c'est possible.
- Certaines valeurs dépendent d'une autre et doivent pouvoir être changées par l'utilisateur : la formation sélectionnée dépend de la catégorie, mais l'utilisateur peut en choisir une autre.

import { Component, computed, linkedSignal, signal } from '@angular/core';
interface Formation {
nom: string;
placesMax: number;
}
const CATALOGUE: Record<string, Formation[]> = {
Web: [{ nom: 'Angular', placesMax: 4 }, { nom: 'React', placesMax: 3 }, { nom: 'Vue', placesMax: 2 }],
Mobile: [{ nom: 'Flutter', placesMax: 5 }, { nom: 'Kotlin', placesMax: 2 }],
Données: [{ nom: 'Python', placesMax: 6 }, { nom: 'SQL', placesMax: 3 }],
};
@Component({
selector: 'app-selection',
template: `
@for (c of categories; track c) {
<button [class.active]="c === categorie()" (click)="categorie.set(c)">{{ c }}</button>
}
<p>
@for (f of formations(); track f.nom) {
<label><input type="radio" name="formation" [checked]="f === choix()" (change)="choix.set(f)" /> {{ f.nom }}</label>
}
</p>
<p>Choix : {{ choix().nom }} – places : {{ places() }} / {{ choix().placesMax }}</p>
<button (click)="places.update((n) => Math.min(n + 1, choix().placesMax))">+1 place</button>
`,
styles: `.active { outline: 3px solid #DD0031; } label { margin-right: 12px; }`,
})
export class Selection {
protected readonly Math = Math;
protected readonly categories = Object.keys(CATALOGUE);
protected readonly categorie = signal('Web');
protected readonly formations = computed(() => CATALOGUE[this.categorie()]);
// Forme courte : revient à la première formation à chaque changement de catégorie,
// mais reste modifiable avec set()
protected readonly choix = linkedSignal(() => this.formations()[0]);
// Forme longue : garde le nombre de places choisi, plafonné par la nouvelle formation
protected readonly places = linkedSignal<Formation, number>({
source: this.choix,
computation: (formation, precedent) => Math.min(precedent?.value ?? 1, formation.placesMax),
});
}
Choix : Angular – places : 1 / 4
(3 × +1 place) Choix : Angular – places : 4 / 4
(React) Choix : React – places : 3 / 3 ← plafonné
(Vue) Choix : Vue – places : 2 / 2
(catégorie Données) Choix : Python – places : 2 / 6 ← nouveau choix, places conservées
(+1 place) Choix : Python – places : 3 / 6
choixrevient à la première formation de la catégorie dès queformations()change, maischoix.set(f)reste possible à tout moment.placesutilise la forme longue :computation(source, precedent)garde le nombre de places choisi (precedent.value) en le plafonnant à la capacité de la nouvelle formation.- Avec un simple
computed, on ne pourrait pas modifier la valeur ; avec unsignalmis à jour par uneffect, on obtiendrait le même résultat de façon plus fragile et plus lente.
-
resource(): des données asynchronesresource(), stable depuis Angular 22, charge une donnée de façon asynchrone et l'expose sous forme de signals :value(),status(),isLoading(),error(),hasValue().- La fonction
paramslit des signals : dès que l'un d'eux change, leloaderest relancé, et le chargement précédent est annulé grâce àabortSignal.reload()relance le chargement à la demande.

- Placez ce fichier
stagiaires.jsondans le dossierpublic/:
[
{ "id": 1, "nom": "Amira Ben Ali", "ville": "Sousse", "moyenne": 16.25 },
{ "id": 2, "nom": "Sami Trabelsi", "ville": "Tunis", "moyenne": 9.5 },
{ "id": 3, "nom": "Ines Gharbi", "ville": "Sfax", "moyenne": 13.75 },
{ "id": 4, "nom": "Youssef Karray", "ville": "Sousse", "moyenne": 11 },
{ "id": 5, "nom": "Salma Jaziri", "ville": "Monastir", "moyenne": 14.5 },
{ "id": 6, "nom": "Karim Mansour", "ville": "Tunis", "moyenne": 12.25 }
]
import { Component, computed, resource, signal } from '@angular/core';
interface Stagiaire {
id: number;
nom: string;
ville: string;
moyenne: number;
}
@Component({
selector: 'app-ressource',
template: `
<select #choix (change)="ville.set(choix.value)">
@for (v of ['Toutes', 'Sousse', 'Tunis', 'Sfax']; track v) { <option [value]="v">{{ v }}</option> }
</select>
<button (click)="stagiaires.reload()">Recharger</button>
<button (click)="fichier.set(fichier() === 'stagiaires.json' ? 'absent.json' : 'stagiaires.json')">Changer de fichier</button>
<p class="statut">statut : {{ stagiaires.status() }} – chargement : {{ stagiaires.isLoading() }}</p>
@if (stagiaires.error()) {
<p class="erreur">Erreur : {{ stagiaires.error()?.message }}</p>
} @else if (stagiaires.hasValue()) {
<ul>
@for (s of stagiaires.value(); track s.id) { <li>{{ s.nom }} ({{ s.ville }}) – {{ s.moyenne }}</li> }
</ul>
<p>{{ nombre() }} stagiaire(s)</p>
}
`,
})
export class Ressource {
protected readonly ville = signal('Toutes');
protected readonly fichier = signal('stagiaires.json');
protected readonly stagiaires = resource({
params: () => ({ ville: this.ville(), fichier: this.fichier() }),
loader: async ({ params, abortSignal }) => {
await new Promise((ok) => setTimeout(ok, 600)); // simule un serveur lent
const reponse = await fetch(params.fichier, { signal: abortSignal });
if (!reponse.ok) throw new Error(`HTTP ${reponse.status} pour ${params.fichier}`);
const liste: Stagiaire[] = await reponse.json();
return params.ville === 'Toutes' ? liste : liste.filter((s) => s.ville === params.ville);
},
});
protected readonly nombre = computed(() => (this.stagiaires.hasValue() ? this.stagiaires.value().length : 0));
}
statut : resolved – chargement : false 6 stagiaires
(ville → Sousse) statut : loading – chargement : true liste vide pendant 0,6 s
statut : resolved – chargement : false
Amira Ben Ali (Sousse) – 16.25 / Youssef Karray (Sousse) – 11 / 2 stagiaire(s)
(Recharger) statut : reloading – chargement : true l'ancienne liste reste affichée
(Changer de fichier) statut : error – chargement : false
Erreur : HTTP 404 pour absent.json
- Pendant un changement de paramètres (
loading), la valeur est vide ; pendant unreload()(reloading), l'ancienne valeur reste disponible. - Le
loaderpeut être n'importe quelle fonctionasync:fetch, un appel à une API, un calcul lourd. Pour les appels HTTP, Angular propose aussihttpResource(), qui utiliseHttpClient(étudié dans un prochain chapitre). - Attention : en état d'erreur, lire
value()lève une exception :ResourceValueError: Resource is currently in an error state (see Error.cause for details). Testezerror()ouhasValue()avant, ou fournissez unedefaultValue. resource()est fait pour lire des données. Pour enregistrer (POST, PUT, DELETE), appelez directement votre service dans une méthode.
-
Signals et RxJS :
toSignal()ettoObservable()- Beaucoup de bibliothèques Angular (le client HTTP, le routeur, les formulaires réactifs) exposent des observables RxJS. Le paquet
@angular/core/rxjs-interopfait le pont :toSignal(observable)transforme un flux en signal,toObservable(signal)fait l'inverse.
- Beaucoup de bibliothèques Angular (le client HTTP, le routeur, les formulaires réactifs) exposent des observables RxJS. Le paquet
import { Component, signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, interval, map } from 'rxjs';
@Component({
selector: 'app-interop',
template: `
<p>Secondes écoulées : {{ secondes() }}</p>
<input #champ (input)="recherche.set(champ.value)" placeholder="Rechercher" />
<p>Saisie : « {{ recherche() }} » – recherche lancée pour : « {{ rechercheStable() }} »</p>
`,
})
export class Interop {
// Observable → signal : désabonnement automatique à la destruction du composant
protected readonly secondes = toSignal(interval(1000).pipe(map((n) => n + 1)), { initialValue: 0 });
// Signal → observable → opérateurs RxJS → signal
protected readonly recherche = signal('');
protected readonly rechercheStable = toSignal(toObservable(this.recherche).pipe(debounceTime(400)), {
initialValue: '',
});
}
Secondes écoulées : 10
(pendant la saisie de « angu ») Saisie : « angu » – recherche lancée pour : « »
(0,4 s après la dernière frappe) Saisie : « angu » – recherche lancée pour : « angu »
toSignals'abonne immédiatement et se désabonne tout seul à la destruction du composant : plus besoin desubscribeni d'unsubscribe. Commeeffect, il doit être appelé dans un contexte d'injection (sinonNG0203: toSignal() can only be used within an injection context …).initialValuedonne la valeur à afficher avant la première émission ; sans elle, le signal vautundefinedau départ.debounceTime(400)attend une pause de 400 ms dans la saisie : c'est le cas typique où les opérateurs RxJS restent plus pratiques que les signals.
-
Les pièges à éviter
- Modifier un objet ou un tableau sur place.
this.liste().push('Sami')modifie le tableau sans prévenir le signal : uncomputed(() => this.liste().length)reste à 1 alors que la liste contient deux noms. Toujours créer une nouvelle valeur :update(l => [...l, 'Sami']),update(o => ({ ...o, age: 3 })). - Oublier les parenthèses dans le template :
{{ compteur }}n'affiche pas la valeur (avertissementNG8109). - Écrire dans un
computed: erreurNG0600. Uncomputedcalcule, il ne modifie rien. - Un effet qui modifie le signal qu'il lit se relance lui-même : sans condition d'arrêt, la page boucle indéfiniment.
- Utiliser
effectpour dériver une valeur: préférezcomputedoulinkedSignal, plus simples, synchrones et sans risque de boucle. - Mélanger signals et propriétés ordinaires pour des données qui changent : une propriété modifiée dans un minuteur ou une promesse n'est pas affichée (voir « Le data binding dans Angular »).
- Modifier un objet ou un tableau sur place.

-
Aide-mémoire

-
Activité 1 : un budget mensuel
- Générer le composant
act-signals/budgetqui suit les dépenses d'un mois. L'état tient en deux signals :budget(1200 DT) etdepenses(un tableau de{ id, libelle, montant, categorie }, avec un loyer de 650 DT et des courses de 180 DT). Tout le reste est calculé aveccomputed:- le total dépensé, le reste (en rouge s'il est négatif) et le pourcentage utilisé, affiché dans une jauge qui passe au rouge à partir de 90 % ;
- la plus grosse dépense, mise en gras dans la liste ;
- le total par catégorie (Logement, Courses, Transport, Loisirs), sans les catégories vides.
- Un formulaire ajoute une dépense (libellé, montant, catégorie) et chaque ligne a un bouton ✕ pour la supprimer. Les ajouts et suppressions utilisent
update()avec un nouveau tableau.
- Générer le composant

- Ajouter Essence (120), Cinéma (45) et Électricité (95), puis passer le budget à 1100 DT : vérifier « 1 090 DT (99 %) – reste : 10 DT ». Supprimer le loyer : quelle dépense passe en gras ?
- Remplacer l'ajout par
this.depenses().push(…): la liste et le total se mettent-ils à jour ? Expliquer.
-
Activité 2 : des préférences enregistrées
- Générer le composant
act-signals/preferences: un signalprefsde type{ theme, taille, langue }, lu au démarrage depuislocalStorage(avec des valeurs par défaut si rien n'est enregistré). Le panneau :- propose une case « Thème sombre », un curseur de taille de texte (12 à 22 px) et une liste de langues (français, arabe, anglais) ; chaque réglage appelle
modifier(changement), qui faitprefs.update(p => ({ ...p, ...changement })); - affiche « Bonjour / مرحبا / Hello, Amira ! » grâce à un
computed, et passe en écriture de droite à gauche en arabe ([attr.dir]) ; - enregistre les préférences dans
localStorageavec un effet, et met à jour le titre de l'onglet avec un second effet; - a un bouton Réinitialiser qui remet les valeurs par défaut.
- propose une case « Thème sombre », un curseur de taille de texte (12 à 22 px) et une liste de langues (français, arabe, anglais) ; chaque réglage appelle
- Générer le composant

- Choisir le thème sombre, 19 px et l'arabe, puis recharger la page: les réglages sont-ils conservés ? Quelle ligne de code les relit ?
- Pourquoi l'accès à
localStorageest-il entouré d'untry … catch? - Pourquoi utiliser
effectici, alors qu'on le déconseille pour calculer des valeurs ?
-
Activité 3 : la réservation d'une salle
- Générer le composant
act-signals/reservation-salle. Trois bâtiments contiennent des salles ({ nom, capacite, equipee }). Utiliser :- un signal
batimentet une caseprojecteur; uncomputedsallesqui ne garde que les salles équipées si la case est cochée ; - un
linkedSignalsallequi revient à la première salle disponible quand la liste change, et que l'utilisateur peut changer en cliquant sur une salle ; - un
linkedSignalparticipants(forme longue) qui conserve le nombre choisi au curseur, plafonné par la capacité de la nouvelle salle ; - un résumé « salle (bâtiment) réservée pour N personne(s), avec projecteur ».
- un signal
- Générer le composant

- Régler 25 participants dans A-101, puis passer au bâtiment B : combien de participants pour B-Amphi ? Puis pour B-204 ? Expliquer avec
precedent.value. - Cocher « Projecteur obligatoire » dans le bâtiment A : quelles salles restent ? Que se passerait-il si aucune salle ne convenait ?
-
Activité 4 : un annuaire avec
resource()- Générer le composant
act-signals/annuairequi chargestagiaires.jsonavecresource()(délai simulé de 500 ms) :- un champ de recherche par nom : c'est le seul paramètre de la
resource, qui renvoie les stagiaires dont le nom contient le texte ; - un curseur « Moyenne ≥ N » appliqué par un
computedsurvalue(), sans recharger les données ; les résultats sont triés par moyenne décroissante ; - un texte d'état « Chargement… » / « N résultat(s) » / « Erreur », un tableau estompé pendant le chargement, un bouton Actualiser (
reload()) désactivé pendant le chargement, et un message d'erreur avec un bouton Réessayer; defaultValue: [], pour quevalue()soit toujours un tableau.
- un champ de recherche par nom : c'est le seul paramètre de la
- Générer le composant

- Pourquoi le curseur de moyenne agit-il instantanément, alors que la recherche par nom affiche « Chargement… » ? Que se passerait-il si
minimum()était lu dansparams? - Taper rapidement « ka » : combien de chargements sont lancés ? Combien vont jusqu'au bout ? Quel paramètre du
loaderle permet ?
-
Activité 5 : un chronomètre
- Générer le composant
act-signals/chronometreavec trois signals :enMarche,ecoule(en centièmes de seconde) ettours(tableau des durées de chaque tour) :- un effet démarre un
setIntervalquandenMarche()est vrai, et l'arrête aveconCleanup; le temps est calculé avecDate.now()pour rester exact ; - l'affichage
mm:ss.ccest uncomputed; les boutons sont Démarrer / Pause / Reprendre, Tour (actif seulement en marche) et Réinitialiser (seulement à l'arrêt) ; - la liste des tours met en vert le meilleur tour, et affiche le meilleur tour et la moyenne (deux
computed) dès qu'il y a deux tours.
- un effet démarre un
- Générer le composant

- Supprimer
onCleanup(() => clearInterval(id)), puis faire plusieurs pauses et reprises : que constatez-vous ? Pourquoi ? - Pourquoi ne pas simplement ajouter 3 centièmes à chaque appel de
setInterval? - Faire un commit après chaque activité, puis un push :
git commit -m "Signals : activité N".
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG0600: Writing to signals is not allowed in a `computed`. Signal: 'journal'. (console) |
set() ou update() dans un computed |
Un computed calcule seulement ; agir dans une méthode ou un effect |
NG0203: effect() can only be used within an injection context … (console) |
effect() créé dans une méthode |
Le créer dans le constructeur ou l'initialisation d'une propriété |
NG0203: toSignal() can only be used within an injection context … (console) |
toSignal() appelé dans une méthode |
L'appeler dans l'initialisation d'une propriété |
TS2339: Property 'set' does not exist on type 'Signal<number>'. |
set() sur un computed ou un signal en lecture seule |
Modifier la source, ou utiliser linkedSignal() |
TS2365: Operator '+' cannot be applied to types 'WritableSignal<number>' and 'number'. |
Signal utilisé sans parenthèses dans le code | this.compteur() + 1 |
NG8109: compteur is a function and should be invoked: compteur() (avertissement) |
Signal sans parenthèses dans le template | {{ compteur() }} |
ResourceValueError: Resource is currently in an error state … (console) |
value() lu alors que la resource est en erreur |
Tester error() / hasValue(), ou defaultValue |
Un computed ou un total ne se met plus à jour |
Tableau ou objet modifié sur place (push, o.x = …) |
update() avec une nouvelle valeur ([...l, x], { ...o, x }) |
Valeur jamais mise à jour malgré set() |
Option equal qui considère les valeurs comme identiques |
Revoir la fonction equal |
| L'effet se relance sans fin | L'effet écrit dans un signal qu'il lit | Lire avec untracked(), ou remplacer par computed / linkedSignal |
| Le chronomètre continue de tourner pendant la pause | setInterval dans un effet sans onCleanup: l'ancien minuteur n'est jamais arrêté |
onCleanup(() => clearInterval(id)) |
| Un effet se relance quand un signal sans rapport change | Signal lu dans l'effet, donc devenu dépendance | Le lire avec untracked() |
-
À retenir
- Un signal contient une valeur et prévient ceux qui l'ont lue : c'est ce qui permet à Angular 22 de mettre à jour exactement ce qui doit l'être.
signal()pour l'état, lu avecs(), modifié avecset()/update()en créant toujours une nouvelle valeur pour les objets et les tableaux.computed()pour les valeurs dérivées (mémorisées, paresseuses, en lecture seule) ;linkedSignal()pour une valeur dérivée que l'utilisateur peut modifier.effect()pour agir sur le monde extérieur, créé dans un contexte d'injection, aveconCleanupetuntrackedsi besoin ; jamais pour calculer.resource()pour les données asynchrones :value(),status(),isLoading(),error(),reload().toSignal()ettoObservable()relient les signals à RxJS.
Un signal pour chaque donnée qui change, un computed pour chaque donnée qui se déduit, un effet seulement pour parler au monde extérieur : avec ces trois réflexes, votre application reste simple, rapide et prévisible.