La leçon 06 t'a donné la valeur (signal) et le dérivé pur (computed). Il manque deux choses pour un vrai composant : faire quelque chose quand une valeur change (logguer, persister, appeler une API) — c'est effect() — et recevoir/émettre des données entre composants parent et enfant — ce sont les signals input() et output().
poste à une ligne enfant (input), l'enfant remonte un clic « éditer » (output). Et certaines réactions sortent du calcul pur : sauvegarder un filtre dans le localStorage, synchroniser l'URL, logguer une alerte — c'est le territoire d'effect(). Sans ces trois briques, tu sais faire un composant isolé, pas une application.
effect() — réagir à un changementUn computed calcule et renvoie une valeur (pur). Un effect ne renvoie rien : il exécute une action à chaque fois qu'un signal qu'il lit change. C'est le bon endroit pour les effets de bord.
Cas concret : on reprend le champ de note avec compteur de l'exemple précédent, et on veut un auto-save du brouillon — comme dans Gmail. L'utilisateur tape, ça se sauve tout seul ; il rafraîchit la page, son texte est encore là. Aucun bouton, aucun timer à câbler.
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-note',
standalone: true,
template: `
<textarea [value]="texte()"
(input)="texte.set($any($event.target).value)"></textarea>
`,
})
export class NoteComponent {
private readonly CLE = 'brouillon-note';
// au démarrage : on RESTAURE le brouillon laissé la dernière fois
texte = signal(localStorage.getItem(this.CLE) ?? '');
constructor() {
// AUTO-SAVE : se rejoue à chaque frappe, sans rien câbler d'autre
effect(() => {
localStorage.setItem(this.CLE, this.texte());
});
}
}
Comme pour computed, les dépendances sont détectées automatiquement : l'effect lit texte(), donc il se rejoue à chaque frappe. Il tourne aussi une première fois au montage. À noter : on le crée dans le constructor (contexte d'injection) ; il se nettoie tout seul quand le composant est détruit. Compare au monde « propriétés nues » : il aurait fallu un handler qui, à chaque input, recompte et ré-écrit dans localStorage à la main. Ici tu déclares la dépendance une fois.
computed (pur, renvoie, mémoïsé). Une action sur le monde extérieur (DOM hors template, localStorage, log, analytics, API) → effect. Règle d'or de revue : un effect ne doit pas servir à calculer un autre signal (écrire un .set() dedans pour dériver une valeur). Si tu fais ça, c'est presque toujours un computed déguisé, et c'est un drapeau rouge.
input() — recevoir une donnée du parentUn composant enfant déclare ce qu'il accepte du parent avec input(). Le résultat est un signal en lecture seule : tu le lis avec (), exactement comme les autres.
Rendons ce champ réutilisable : un <app-champ-texte> qu'on pose n'importe où, en lui passant son libellé et sa limite. Ce qui change d'un usage à l'autre devient des input().
import { Component, input, signal, computed } from '@angular/core';
@Component({
selector: 'app-champ-texte',
standalone: true,
template: `
<label>{{ label() }}</label>
<textarea [value]="texte()"
(input)="texte.set($any($event.target).value)"></textarea>
<small [class.ko]="trop()">{{ restants() }} restants</small>
`,
})
export class ChampTexteComponent {
label = input.required<string>(); // requis : le parent DOIT le fournir
max = input(280); // optionnel, valeur par défaut
texte = signal('');
restants = computed(() => this.max() - this.texte().length); // un input nourrit un computed
trop = computed(() => this.restants() < 0);
}
Côté parent, on alimente l'input via le property binding de la leçon 04. Note la nuance : sans crochets c'est une chaîne littérale, avec crochets c'est une expression.
<app-champ-texte label="Commentaire d'alerte" [max]="280" />
input(), c'est un paramètre du composant : comme les arguments d'un constructeur ou les propriétés publiques d'une classe que l'appelant renseigne. input.required<T>() = un paramètre obligatoire (oubli = erreur de compilation). La nouveauté vs l'ancien @Input() : ici l'input est un signal, donc tu peux le brancher direct dans un computed ou un effect — la réactivité traverse la frontière parent → enfant sans glu.
output() — émettre vers le parentL'enfant ne modifie jamais directement le parent. Il émet un événement via output(), et le parent décide quoi en faire. Sens : enfant → parent (l'event binding de la leçon 04, côté composant).
On ajoute un bouton « Enregistrer » au champ. Quand on clique, l'enfant n'écrit rien lui-même : il émet le texte saisi et laisse le parent décider.
// dans ChampTexteComponent — on ajoute l'output et le bouton
valide = output<string>(); // émet le texte saisi
valider() {
this.valide.emit(this.texte()); // on remonte la donnée au parent
this.texte.set(''); // puis on vide le champ
}
// template : <button [disabled]="trop() || texte().length === 0"
// (click)="valider()">Enregistrer</button>
Côté parent, on écoute l'output comme un événement et on collecte la note dans la liste :
@Component({
selector: 'app-fiche-poste',
standalone: true,
imports: [ChampTexteComponent],
template: `
<app-champ-texte label="Commentaire d'alerte"
(valide)="ajouter($event)" />
<ul>
@for (note of notes(); track $index) { <li>{{ note }}</li> }
</ul>
`,
})
export class FichePosteComponent {
notes = signal<string[]>([]);
ajouter(texte: string) {
this.notes.update(liste => [...liste, texte]); // événement reçu → état mis à jour
}
}
Le parent reçoit la donnée émise dans $event. C'est le pendant exact de l'input : [ ] fait descendre la donnée, ( ) fait remonter l'événement. Données vers le bas, événements vers le haut : c'est le contrat de communication entre composants. Remarque comme tout se recolle : l'input nourrit un computed (leçon 06), l'output déclenche un update sur un signal, et le @for (leçon 05) réaffiche la liste — sans une ligne de synchro manuelle.
| Primitive | Rôle | Sens |
|---|---|---|
signal(v) | valeur source, modifiable | interne |
computed(fn) | valeur dérivée pure (lecture seule) | interne |
effect(fn) | action / effet de bord au changement | vers l'extérieur |
input() / output() | recevoir / émettre | parent ↔ enfant |
effect() que pour de vrais effets de bord. Dériver une valeur dans un effect (avec un .set()) au lieu d'un computed = le bug d'architecture le plus fréquent en revue signals (recalculs en cascade, ordre imprévisible). (2) Un input() est en lecture seule : l'enfant ne réécrit jamais dedans, il émet via un output et laisse le parent trancher. (3) Préfère input.required<T>() quand la donnée est indispensable : l'oubli est attrapé à la compilation, pas en prod. (4) Nomme les output par l'intention métier (editer, supprimer), pas par le mécanisme (onClick).
localStorage dès qu'il change. Tu utilises ?autreSignal.set(...) dans un effect() ?signal (valeur source), computed (valeur dérivée pure), effect (action / effet de bord rejouée au changement, dépendances auto-détectées, créée dans le constructor), et la paire input() / output() (recevoir du parent / émettre vers lui). Contrat de communication : données vers le bas ([input]), événements vers le haut ((output)). Réflexe de lead : valeur → computed, action → effect (jamais un .set() dérivé dedans), input.required pour l'indispensable.
Angular — Signals (sections Effects) puis input() et output(). La page Components montre le couple input/output en contexte.
effect qui synchronise un filtre avec l'URL ? Demande-moi, ici, maintenant.