Tu as les opérateurs (leçon 09), mais deux questions restent ouvertes : comment fabriquer un Observable qu'on pousse soi-même (une frappe, un clic), et comment le brancher au template sans subscribe manuel (donc sans fuite). Réponses : le Subject, l'async pipe, et les deux ponts toSignal / toObservable qui relient RxJS et signals. Cette leçon boucle l'auto-save serveur laissé en suspens.
Subject — un Observable que tu pilotes à la mainJusqu'ici les Observables t'étaient donnés (par HttpClient, par un opérateur). Un Subject est un Observable dont tu contrôles les émissions : tu appelles .next(valeur) et tous les abonnés la reçoivent. C'est le pont entre le monde impératif (« ce clic vient d'arriver ») et le monde des flux.
import { Subject } from 'rxjs';
const saisie$ = new Subject<string>();
saisie$.subscribe(t => console.log('reçu :', t));
saisie$.next('Pos'); // → "reçu : Pos"
saisie$.next('Poste'); // → "reçu : Poste"
Un Subject ordinaire n'a pas de valeur courante : qui s'abonne trop tard rate ce qui est déjà passé. Sa variante BehaviorSubject corrige ça — elle exige une valeur initiale et rejoue toujours la dernière valeur à tout nouvel abonné. Pour un état (le filtre courant, le poste sélectionné), c'est presque toujours le BehaviorSubject qu'on veut.
import { BehaviorSubject } from 'rxjs';
const filtre$ = new BehaviorSubject<string>('tous'); // valeur initiale obligatoire
filtre$.next('actifs');
filtre$.value; // 'actifs' — on peut lire la valeur courante, comme un signal()
Subject, c'est ton event / EventHandler C# côté RxJS : .next(x) ≈ lever l'événement, subscribe ≈ += un handler. Et le BehaviorSubject est troublant de ressemblance avec un signal : une valeur courante lisible (.value) plus un flux de changements. La différence : le signal est fait pour le rendu synchrone dans le template ; le BehaviorSubject est fait pour être composé avec des opérateurs (debounceTime, switchMap…).
async pipe — afficher un flux sans subscribeSouviens-toi : un Observable est paresseux, rien ne tourne sans subscribe. Mais s'abonner à la main dans le composant, c'est s'obliger à se désabonner à la destruction — sinon fuite mémoire. L'async pipe fait les deux pour toi, dans le template : il s'abonne, affiche la dernière valeur émise, et se désabonne tout seul quand le composant meurt.
@Component({
selector: 'app-liste-postes',
standalone: true,
imports: [AsyncPipe],
template: `
<!-- postes$ est un Observable<Poste[]> ; async s'abonne pour toi -->
@if (postes$ | async; as postes) {
<ul>
@for (p of postes; track p.id) { <li>{{ p.nom }}</li> }
</ul>
} @else {
<p>Chargement…</p>
}
`,
})
export class ListePostesComponent {
private api = inject(PosteApi);
postes$ = this.api.lister(); // Observable — AUCUN subscribe ici
}
Le | async déclenche l'abonnement, et le as postes range la valeur émise dans une variable de template réutilisable. Tant que rien n'est arrivé, async renvoie null → la branche @else montre « Chargement… ». Un seul abonnement, désabonnement garanti, zéro ligne de nettoyage.
flux$.subscribe(...) dans un composant, sans désabonnement, continue de tourner après la destruction — mémoire qui gonfle, callbacks fantômes. Nuance : ça vise les flux infinis (Subject, interval, valueChanges) ; un flux qui se complète, comme un http.get(), libère son abonnement tout seul (pas de fuite). Règle : dans un template, préfère toujours l'async pipe à un subscribe manuel. Si tu dois vraiment t'abonner en TypeScript, le réflexe moderne est takeUntilDestroyed() (nettoyage lié au cycle de vie). Voir un .subscribe( nu dans une PR = question automatique : « qui le désabonne ? ».
toSignal / toObservable — les deux pontsVoici la jonction propre entre les deux mondes, dans @angular/core/rxjs-interop :
toObservable(sig) : prend un signal → renvoie un Observable qui émet à chaque changement. C'est ce qui manquait leçon 09 pour fabriquer saisie$ à partir du champ.toSignal(obs$) : prend un Observable → renvoie un signal en lecture seule qui vaut toujours la dernière valeur émise. Il gère l'abonnement et le désabonnement pour toi (comme l'async pipe, mais côté TypeScript).On boucle enfin le fil rouge : la note du champ est un signal (leçon 07), on la transforme en flux, on la débounce et on la sauve serveur avec switchMap (leçon 09), puis on ramène l'état de sauvegarde en signal pour l'afficher — sans un seul subscribe manuel.
import { Component, signal } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, map, filter, switchMap, startWith } from 'rxjs';
@Component({
selector: 'app-note-poste',
standalone: true,
template: `
<textarea [value]="texte()"
(input)="texte.set($any($event.target).value)"></textarea>
<small>{{ etat() }}</small> <!-- un signal : lecture directe, sans async -->
`,
})
export class NotePosteComponent {
private api = inject(PosteApi);
texte = signal(''); // état local → signal (leçon 07)
// pont 1 : le signal devient un flux qu'on peut composer
private saisie$ = toObservable(this.texte);
// pipeline de la leçon 09, enfin alimenté par une vraie source
private sauvegarde$ = this.saisie$.pipe(
debounceTime(400),
map(t => t.trim()),
filter(t => t.length > 0),
switchMap(t => this.api.sauver(t)), // annule la sauvegarde en vol
map(() => 'enregistré ✓'),
startWith('—'), // valeur affichée avant la 1re sauvegarde
);
// pont 2 : le flux redevient un signal, prêt pour le template
etat = toSignal(this.sauvegarde$, { initialValue: '—' });
}
Regarde le trajet : signal → toObservable → opérateurs RxJS → toSignal → template. Le signal fait ce qu'il fait de mieux (état synchrone lisible), RxJS fait ce qu'il fait de mieux (debounce + annulation), et les deux ponts assurent la circulation. Aucun subscribe, aucun désabonnement à écrire : toSignal vit et meurt avec le composant.
debounceTime/switchMap (que les signals ne savent pas faire) → on revient en signal pour l'affichage. On ne convertit pas « pour convertir » : on emprunte RxJS pour l'opérateur dont on a besoin, puis on rend la main aux signals.
| Besoin | Outil | Pourquoi |
|---|---|---|
| État local, compteur, filtre courant | signal | synchrone, lisible direct dans le template |
| Valeur dérivée pure | computed | mémoïsé, pas de flux nécessaire |
| Debounce, throttle, annulation | RxJS + opérateurs | les signals n'ont pas la notion de temps |
| Flux temps réel (WebSocket, mesures) | RxJS | événements multiples dans le temps |
| Appel HTTP / GraphQL | RxJS → toSignal | c'est un Observable ; on le ramène en signal pour l'afficher |
| Afficher un Observable | async pipe ou toSignal | abonnement + désabonnement gérés pour toi |
Subject vs BehaviorSubject : dès qu'il y a une valeur courante à connaître (état), c'est BehaviorSubject (ou carrément un signal). Subject nu = flux d'événements purs, sans mémoire. (2) Ne fais pas cohabiter deux sources de vérité pour la même donnée (un signal et un BehaviorSubject qu'on synchronise à la main) : choisis-en une, ponte l'autre à la demande. (3) toSignal remplace l'écrasante majorité des subscribe manuels que tu écrivais en Angular 14 — si tu en vois encore beaucoup, c'est du code d'avant l'interop. (4) Un toObservable émet la valeur courante puis les changements : pense au debounceTime pour ne pas déclencher un appel sur l'état initial.
postes$ | async par un subscribe dans le constructeur qui stocke le tableau. Qu'est-ce que l'async pipe apportait vraiment que ce code perd ?signal. Tu veux débouncer 400 ms puis sauver serveur en annulant la sauvegarde en vol. Quelle est la faille de l'approche « tout signal, tout dans un effect » ?signal piloté par effect ne recharge qu'une fois ; un Subject recharge deux fois. Pourquoi ?Subject = un Observable que tu pousses toi-même (.next()) ; BehaviorSubject = idem avec valeur initiale et dernière valeur rejouée (proche d'un signal). L'async pipe s'abonne et se désabonne pour toi dans le template — l'anti-fuite par défaut. Les ponts toObservable (signal → flux, pour emprunter les opérateurs) et toSignal (flux → signal, pour afficher sans subscribe) relient les deux mondes. Doctrine : signal pour l'état, RxJS pour l'asynchrone événementiel, on ne convertit que là où c'est utile.
Angular — RxJS Interop (toSignal / toObservable), puis l'async pipe. Côté RxJS, la fiche Subject (et sa section BehaviorSubject).
api.sauver par un vrai HttpClient pour voir toSignal avaler une réponse HTTP, ou qu'on compare côte à côte le même écran écrit « tout signal » vs « tout Observable + async » pour sentir où chacun gagne ? Demande-moi, ici, maintenant.