Interop signals & RxJS
À retenir —
toSignal()transforme un Observable en signal (abonnement géré automatiquement),toObservable()fait l'inverse. Signals et RxJS sont complémentaires : signals pour l'état synchrone, RxJS pour les flux asynchrones.
Les signals ne remplacent pas RxJS — ils le complètent. Une application réelle combine les deux : RxJS pour orchestrer l'asynchrone (HTTP, websockets, événements debounce), signals pour l'état qui pilote l'affichage. Le pont entre les deux mondes se trouve dans @angular/core/rxjs-interop.
toSignal() : d'un Observable vers un signal
toSignal() s'abonne à un Observable et expose sa dernière valeur sous forme de signal. L'abonnement est créé à l'appel et détruit automatiquement quand le contexte d'injection disparaît — fini les unsubscribe manuels et l'async pipe.
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval, map } from 'rxjs';
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>Chronomètre</h1>
<p>Secondes écoulées : {{ secondes() }}</p>
`,
})
export class AppComponent {
readonly secondes = toSignal(
interval(1000).pipe(map((n) => n + 1)),
{ initialValue: 0 },
);
}
Essayez en direct
rxjs-tosignal
Gérer la valeur initiale
Un Observable peut n'émettre qu'après un délai. Avant la première émission, le signal a besoin d'une valeur. Trois stratégies :
// 1. Fournir une valeur initiale explicite (recommandé) :
readonly data = toSignal(flux$, { initialValue: [] });
// 2. Accepter undefined avant la première émission :
readonly data = toSignal(flux$); // Signal<T | undefined>
// 3. Exiger une émission synchrone immédiate (sinon erreur) :
readonly data = toSignal(flux$, { requireSync: true });
toObservable() : d'un signal vers un Observable
À l'inverse, toObservable() transforme un signal en Observable. Pratique pour réutiliser la puissance des opérateurs RxJS (debounceTime, switchMap, combineLatest…) à partir d'un état en signal.
import { Component, signal, inject, ChangeDetectionStrategy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { Observable, debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
@Component({ /* … */ })
export class RechercheComponent {
private readonly http = inject(HttpClient);
readonly terme = signal('');
// Le signal devient un flux : on "debounce" la frappe puis on déclenche
// une recherche HTTP à chaque terme stabilisé.
private readonly resultats$ = toObservable(this.terme).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.rechercher(q)),
);
// …puis on revient au monde des signals pour l'affichage.
readonly resultats = toSignal(this.resultats$, { initialValue: [] });
private rechercher(q: string): Observable<string[]> {
return this.http.get<string[]>(`/api/search?q=${q}`);
}
}
Ce va-et-vient — signal → observable (pour les opérateurs) → signal (pour l'affichage) — est l'un des patterns les plus utiles de l'interop.
resource() et rxResource() : l'asynchrone réactif
Angular fournit une API dédiée au chargement de données asynchrones piloté par des signals : resource() (et sa variante RxJS rxResource()). Une resource relance automatiquement sa requête quand ses signals sources changent, et expose l'état (value, status, error) sous forme de signals.
import { resource, signal } from '@angular/core';
const userId = signal(1);
const utilisateur = resource({
// Quand request change, le loader est relancé.
request: () => ({ id: userId() }),
loader: async ({ request, abortSignal }) => {
const res = await fetch(`/api/users/${request.id}`, { signal: abortSignal });
return res.json();
},
});
// Lecture réactive :
utilisateur.value(); // les données (ou undefined pendant le chargement)
utilisateur.status(); // 'idle' | 'loading' | 'resolved' | 'error' …
utilisateur.isLoading();
resource()est récent (Angular 19, en developer preview). L'API peut encore évoluer ; vérifiez la version d'Angular de votre projet avant de l'employer en production. Pour un besoin simple,toSignal(this.http.get(...))reste parfaitement valable.
Quand utiliser quoi ?
| Situation | Outil recommandé |
|---|---|
| État synchrone qui pilote l'UI (compteur, sélection, formulaire) | signal / computed |
Frappe clavier avec debounce, anti-rebond, fusion de flux |
RxJS (puis toSignal pour l'affichage) |
| Requête HTTP unique affichée dans le template | toSignal(http.get(...)) |
| Requête qui se relance selon des paramètres réactifs | resource() / rxResource() |
| Websocket, événements continus | RxJS |
Piège classique : créer toSignal/toObservable hors contexte d'injection
Comme effect(), ces fonctions ont besoin d'un contexte d'injection pour gérer le cycle de vie de l'abonnement. Déclarez-les en initialiseur de champ ou dans le constructeur — pas dans ngOnInit ni dans un gestionnaire d'événement. Si vous devez vraiment le faire ailleurs, passez un injector :
constructor(private injector: Injector) {}
ngOnInit() {
this.valeur = toSignal(this.flux$, { initialValue: 0, injector: this.injector });
}
En résumé
toSignal(): Observable → signal, abonnement auto-géré, gère la valeur initiale.toObservable(): signal → Observable, pour réutiliser les opérateurs RxJS.resource()/rxResource(): chargement asynchrone réactif piloté par signals (Angular 19, preview).- Créez-les dans un contexte d'injection.
Au chapitre 8, on relie tout cela au rendu : le nouveau control flow (@if, @for), la stratégie OnPush et le mode zoneless que les signals rendent possible.