Interop signals & RxJS

À retenirtoSignal() 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.

Nous utilisons Microsoft Clarity pour comprendre comment le site est utilisé et l'améliorer. En poursuivant votre navigation, vous l'acceptez. Vous pouvez le désactiver à tout moment.