← Retour au blog
AngularRxJSSignalsMigrationReactive Programming

RxJS vers Signals : maîtriser toSignal et toObservable pour une migration progressive

Angular 16 a introduit les Signals comme primitive réactive fondamentale, mais RxJS reste omniprésent dans les codebases existantes. La vraie question n'est pas « Signals ou RxJS ? » mais plutôt « Comment les faire coexister intelligemment ? ». Les fonctions toSignal et toObservable sont vos ponts de transition. Elles permettent de migrer graduellement sans refondre votre architecture d'un coup. Comprendre leurs coûts, leurs limitations et leurs cas d'usage spécifiques est essentiel pour écrire du code maintenable et performant.

Le contexte : pourquoi cette dualité ?

RxJS est basé sur l'observation déclarative et les opérateurs enchaînés. Les Signals sont une approche plus directe : réactivité fine à la source, sans overhead d'observable. Angular a choisi une stratégie pragmatique : les Signals deviennent la couche réactive de base (détection de changement, stores, etc.), mais RxJS reste utile pour les flux complexes, les timers, les requêtes HTTP. Vous n'êtes pas forcé de tout réécrire. Les équipes qui ont des observables imbriquées, des opérateurs sophistiqués ou des dépendances RxJS lourdes bénéficient d'une migration progressive. L'idée est de tirer parti des Signals pour la nouvelle logique métier tout en gardant RxJS où il excelle vraiment.

toSignal : convertir un observable en signal

toSignal prend un observable et retourne un signal qui émet la dernière valeur reçue. C'est l'outil pour intégrer des sources RxJS dans votre logique Signal. La signature est simple : toSignal(observable, options?). Vous pouvez passer une valeur initiale via options.initialValue, ce qui évite les undefined pendant le chargement. Exemple concret : vous avez un service HTTP qui retourne un observable, et vous voulez l'utiliser dans une computed ou un effect Signal.

import { toSignal } from '@angular/core';

import { HttpClient } from '@angular/common/http';

export class UserStore {

private http = inject(HttpClient);

private userObservable$ = this.http.get('/api/user');

// Convertir en Signal

user = toSignal(this.userObservable$, { initialValue: null });

// Utiliser dans une computed

userDisplay = computed(() => {

const u = this.user();

return u ? ${u.name} (${u.role}) : 'Chargement...';

});

}

Attention à la souscription : toSignal crée automatiquement une souscription interne gérée par Angular. Vous ne devez pas vous-même vous abonner à l'observable passé. Le signal reflète toujours la dernière valeur, ce qui convient parfaitement aux données simple comme les préférences utilisateur, les données de formulaire ou les réponses API. Si vous avez besoin de réagir aux changements avec des side-effects complexes, un effect est plus approprié qu'une computed.

toObservable : convertir un signal en observable

C'est l'inverse : toObservable prend un signal et retourne un observable qui émet chaque fois que le signal change. Utile quand vous avez une logique réactive basée sur Signals mais que vous devez la passer à une librairie ou un opérateur RxJS. Par exemple, intégrer avec ngxs, rxjs-operators complexes, ou une API tierce qui n'attend qu'un observable.

import { toObservable } from '@angular/core';

import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class SearchComponent {

query = signal('');

// Convertir le signal en observable pour appliquer des opérateurs RxJS

results$ = toObservable(this.query).pipe(

debounceTime(300),

distinctUntilChanged(),

switchMap(q => this.searchService.find(q))

);

}

Ici, le signal query est converti en observable pour bénéficier des opérateurs debounceTime et switchMap. Chaque modification du signal émet une nouvelle valeur dans l'observable, déclenchant le pipeline RxJS. C'est élégant et lisible : la source est un Signal, la transformation est RxJS, et vous pouvez consommer le résultat via async pipe ou toSignal à nouveau.

Cas d'usage réalistes et choix architecturaux

Une composante de filtre avec recherche en temps réel bénéficie d'une approche mixte. Le terme de recherche est un signal (réactif, rapide), mais la requête HTTP et le debouncing passent par RxJS. Vous finissez avec un observable des résultats, que vous reconvertissez en signal pour éviter async pipe et faciliter les computed. Pour un store NgRx-like maison, vous pouvez stocker l'état en Signals et exposer des sélecteurs en observables via toObservable, maintenant une API Observable-first pour les consommateurs existants tout en bénéficiant des Signals en interne.

Une autre pattern : les dépendances entre signaux et observables. Si un signal dépend d'un observable (toSignal), et qu'un observable dépend d'un signal (toObservable), vous créez une chaîne réactive cohérente. Attention toutefois à ne pas créer de boucles infinies : un signal qui déclenche un observable qui met à jour le signal peut causer des mises à jour excessives. Utilisez distinctUntilChanged ou des guards explicites pour éviter cela.

Les pièges courants

Le premier piège : oublier que toSignal crée une souscription. Si vous appelez toSignal sur un observable froid (comme httpClient.get), la requête se lance au moment du création du signal, même si personne ne consomme le signal. Pour un observable chaud, c'est correct. Pour du lazy-loading, il faut penser à utiliser un lazy signal ou à déclencher manuellement. Deuxième piège : l'initialValue. Si vous ne passez pas initialValue, le signal commence undefined jusqu'à la première émission de l'observable. Cela peut casser votre logique métier si vous supposez une valeur présente. Toujours définir une valeur par défaut pertinente.

Le troisième piège : les performances. Convertir un observable qui émet rapidement en signal peut surcharger la détection de changement Angular. Si vous avez un observable qui émet 100 fois par seconde (capteur, WebSocket), toSignal va mettre à jour le signal 100 fois par seconde, déclenchant à chaque fois le scheduler de détection de changement. Utilisez des opérateurs RxJS comme throttleTime ou debounceTime avant toSignal pour limiter les émissions. Enfin, les dépendances circulaires entre signaux et observables sont subtiles mais dangereuses : une signal qui déclenche un observable qui met à jour le signal peut créer des boucles infinies. Toujours tracer le flux de données et utiliser les devtools Angular pour déboguer.

Stratégie de migration progressive

Commencez par identifier les points d'entrée : les services HTTP, les formulaires réactifs, les stores. Pour chaque service HTTP, créez une version qui expose toSignal, en plus de l'observable. Cela permet aux nouvelles composantes d'utiliser les Signals sans impacter les anciennes. Pour les formulaires, FormGroup.valueChanges reste un observable, mais vous pouvez le convertir via toSignal pour les composantes qui utilisent Signals. Les stores NgRx peuvent exposer à la fois des observables (via les sélecteurs existants) et des signaux (via toSignal des sélecteurs).

Une approche réaliste : hybrider par domaine métier. Si vous avez un module de gestion utilisateur, migrez ses composantes et son store en Signals, en exposant une API observables-compatible via toObservable. Les autres modules continuent avec RxJS. Progressivement, vous montez en Signals. Cette stratégie réduit le risque, permet de valider le pattern sur un petit périmètre, et facilite la formation de l'équipe.

Conclusion opérationnelle

toSignal et toObservable ne sont pas des outils de migration complète, mais des ponts pragmatiques. Utilisez toSignal pour intégrer des observables dans votre logique Signal, en veillant à bien gérer l'initialValue et la souscription. Utilisez toObservable pour exposer des signaux à des code ou des librairies qui attendent un observable. Évitez les conversions inutiles : si vous restez dans l'univers Signal, restez-y. Si vous restez dans RxJS, restez-y. Les conversions ont un coût, même mineur. Planifiez une migration progressive par domaine, pas un big-bang. Et validez votre architecture réactive avec Angular DevTools et un profiling attentif des mises à jour de détection de changement. L'objectif est un code plus maintenable et performant, pas une adoption dogmatique des Signals.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️