Les signals ne remplacent pas RxJS — ils le complètent. Les signals sont excellents pour l’état (ce qui est affiché maintenant), RxJS pour les événements dans le temps (ce qui se passe et quand). Le package @angular/core/rxjs-interop fournit les passerelles entre les deux mondes. Nouveau avec les Observables ? Commencez par Les Observables depuis zéro.
| Signals | RxJS / Observable |
|---|---|
| Ont toujours une valeur courante | Émettent des valeurs dans le temps (elles peuvent ne pas avoir encore émis) |
Synchrone : count() | Asynchrone : subscribe() |
| Aucun désabonnement nécessaire | Nécessitent un désabonnement (ou un opérateur qui se termine) |
Dérivation : computed() | Dérivation : map, combineLatest, switchMap… |
| Idéal pour les templates et l’état de l’UI | Idéal pour les websockets, le drag, le retry, les debounce complexes |
toSignal() : Observable → Signal
import { toSignal } from '@angular/core/rxjs-interop';
export class Clock {
// Abonnement automatique + désabonnement automatique à la destruction du composant
readonly seconds = toSignal(interval(1000), { initialValue: 0 });
// Sans initialValue → le type est T | undefined
readonly user = toSignal(this.auth.user$); // Signal<User | undefined>
// requireSync : l’observable émet de façon synchrone (BehaviorSubject, startWith) → pas d’undefined
readonly theme = toSignal(this.theme$, { requireSync: true }); // Signal<Theme>
// Paramètres de route en signal (si vous n’utilisez pas withComponentInputBinding)
private readonly route = inject(ActivatedRoute);
readonly id = toSignal(this.route.paramMap.pipe(map((p) => Number(p.get('id')))), {
initialValue: 0,
});
}toObservable() : Signal → Observable
import { toObservable } from '@angular/core/rxjs-interop';
export class ProductSearch {
readonly query = signal('');
// signal → observable → opérateurs RxJS → retour à un signal
readonly results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
filter((q) => q.length >= 2),
switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
),
{ initialValue: [] },
);
}
// toObservable émet de façon ASYNCHRONE (via un effect) et seulement la valeur finale
// si le signal est modifié plusieurs fois dans le même tick.appuyez sur le boutonLà où RxJS reste meilleur
Un flux mousemove avec throttle et accumulation (scan) est trivial en RxJS et laborieux avec des signals. Vous placez le résultat final dans un signal pour le template.
0, 0 · événements traités (throttle 40 ms) : 0takeUntilDestroyed() : adieu le Subject destroy$
Chaque subscribe() manuel dans un composant doit être arrêté à la destruction. takeUntilDestroyed() le fait automatiquement grâce à DestroyRef. (Avec toSignal, vous n’en avez pas besoin — il se désabonne tout seul.)
import { DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class Notifications {
private readonly destroyRef = inject(DestroyRef);
constructor() {
// ✓ dans un contexte d’injection : aucun argument
this.socket.messages$.pipe(takeUntilDestroyed()).subscribe((m) => this.show(m));
}
connectLater() {
// ✓ hors du constructeur : passez le DestroyRef explicitement
this.poll$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
}
}
// Avant : Subject + ngOnDestroy
// private destroy$ = new Subject<void>();
// ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
// obs$.pipe(takeUntil(this.destroy$))- Créez un enfant, puis détruisez-le et observez le journal.
Outputs depuis et vers des Observables
import { outputFromObservable, outputToObservable } from '@angular/core/rxjs-interop';
export class Search {
private readonly query$ = new Subject<string>();
// Un output construit à partir d’un Observable
readonly queryChange = outputFromObservable(this.query$.pipe(debounceTime(300)));
}
// Dans l’autre sens : vous traitez l’output d’un composant comme un Observable (p. ex. dans une directive)
const changes$ = outputToObservable(childComponentRef.instance.queryChange);Recommandation d’architecture
// La recommandation d’architecture :
// RxJS aux frontières (I/O, événements), Signals pour l’état affiché.
@Service()
export class ChatStore {
private readonly socket = inject(ChatSocket); // expose Observable<Message>
// un flux d’événements → accumulé avec RxJS → exposé en signal
readonly messages = toSignal(
this.socket.messages$.pipe(scan((all, m) => [...all, m].slice(-100), [] as Message[])),
{ initialValue: [] },
);
readonly unread = computed(() => this.messages().filter((m) => !m.read).length);
}Quand OUI, quand NON
- Vous avez des événements dans le temps : websocket, SSE, drag & drop, scroll infini.
- Vous avez besoin d’opérateurs de temps/concurrence :
switchMap,exhaustMap,retryavec backoff,bufferTime. - Vous combinez de nombreuses sources asynchrones avec des règles complexes.
- Vous travaillez avec des API qui exposent des Observables (événements du Router,
valueChangesdes Reactive Forms).
- Vous gérez un état d’UI (filtres, sélection, bascules).
- Vous dérivez des valeurs pour le template —
computedest plus simple quecombineLatest. - Vous faites des GET qui dépendent de l’état —
httpResource. - Vous utilisez un
BehaviorSubjectuniquement comme « variable observable » — unsignalest exactement cela.
Pièges fréquents
toSignalhors du contexte d’injection → une erreur. Créez-le comme champ ou passez{ injector }.toSignalsur un Observable HTTP « cold » dans un getter → une nouvelle requête à chaque appel. Créez-le une seule fois.- Erreurs : si l’Observable émet une erreur, la lecture du signal lève une exception. Interceptez d’abord l’erreur avec
catchError. toObservablen’émet pas de façon synchrone : ne comptez pas sur la valeur juste aprèsset().
Aide-mémoire
toSignal(obs$, { initialValue }) // Observable → Signal (désabonnement auto)
toSignal(obs$, { requireSync: true }) // pas d’undefined, s’il émet de façon synchrone
toObservable(sig) // Signal → Observable (émet de façon asynchrone)
takeUntilDestroyed() // dans un contexte d’injection
takeUntilDestroyed(destroyRef) // n’importe où
outputFromObservable(obs$) // Observable → output()
outputToObservable(output) // output() → Observable
rxResource({ params, stream }) // données asynchrones via un Observable → Resource
pendingUntilEvent() // garde l’application « instable » (SSR) jusqu’à la première émission