ITSkillsCenter
Blog

Angular signals et RxJS en pratique : guide pratique

12 دقائق للقراءة

Lecture : 13 minutes · Niveau : intermédiaire-avancé · Mise à jour : avril 2026

Angular en 2026 propose deux modèles réactifs côte à côte : les signals (modernes, simples, synchrones) et RxJS (puissant, asynchrone, mature). Ne pas confondre les deux et savoir quand utiliser lequel est la clé pour écrire du code Angular moderne efficace. Ce guide décortique chaque modèle, leurs forces, leurs interactions et les patterns qui marchent.

L’arrivée des signals a profondément modifié la pratique Angular. Avant 2024, RxJS était omniprésent : BehaviorSubject pour partager de l’état, async pipe partout, opérateurs de composition pour les calculs dérivés. Cette approche était puissante mais exigeait une vraie maîtrise de RxJS, ce qui ralentissait l’onboarding des nouveaux développeurs et produisait du code parfois sur-réactif. Les signals simplifient drastiquement les cas synchrones tout en laissant RxJS régner sur l’asynchrone. Cette nouvelle division du travail rend Angular plus accessible sans sacrifier la puissance qui fait sa réputation.

Voir aussi → Angular pour entreprise : guide pratique.


Sommaire

  1. Signals vs RxJS : la division du travail
  2. Signals : les trois primitives
  3. Patterns courants avec signals
  4. RxJS : les opérateurs qui comptent vraiment
  5. HTTP avec RxJS
  6. Interop : toSignal et toObservable
  7. Pièges classiques
  8. Performance et change detection
  9. FAQ

1. Signals vs RxJS : la division du travail

Le bon réflexe en 2026 : commencer par les signals, descendre à RxJS quand vraiment nécessaire.

Signals pour :
– État UI synchrone (compteur, mode édition, valeurs de filtres)
– Données dérivées calculables synchrones
– État de composant
– Communication parent-enfant via inputs/outputs en signals
– État partagé entre composants via services exposant des signals

RxJS pour :
– Requêtes HTTP (HttpClient retourne des Observables)
– Événements DOM ou WebSocket
– Combinaisons asynchrones complexes (debounce, throttle, retry, switchMap)
– Streams temporels (intervals, timers)
– Backpressure et gestion de flux

Beaucoup de cas qu’on faisait avec RxJS avant 2024 (subjects pour partager de l’état) sont maintenant plus simples avec des signals. Les Observables restent pertinents pour ce qui est vraiment asynchrone et continu.


2. Signals : les trois primitives

signal()

Conteneur de valeur réactive.

import { signal } from "@angular/core";

const count = signal(0);

count();              // lecture : retourne 0
count.set(5);         // écriture directe
count.update(c => c + 1);  // écriture basée sur la valeur précédente

Dans un template :

<p>Compteur : {{ count() }}</p>

L’appel count() lit la valeur. Si elle change, Angular re-rend automatiquement le fragment du template qui en dépend.

computed()

Valeur dérivée. Recalculée automatiquement quand les dépendances changent. Mémorisée : si on la lit plusieurs fois sans que les deps changent, le calcul ne refait pas.

const items = signal<Item[]>([]);
const search = signal("");

const filtered = computed(() =>
  items().filter(item => item.nom.includes(search()))
);

// Dans le template
@for (item of filtered(); track item.id) {
  <li>{{ item.nom }}</li>
}

filtered se recalcule uniquement quand items ou search change.

effect()

Exécute du code à chaque changement des signals utilisés. Pour des effets de bord (logging, sync localStorage, appels imperatifs).

import { effect } from "@angular/core";

@Component(...)
export class MyComponent {
  user = signal<User | null>(null);

  constructor() {
    effect(() => {
      const u = this.user();
      if (u) {
        console.log(`Utilisateur : ${u.nom}`);
        localStorage.setItem("lastUser", JSON.stringify(u));
      }
    });
  }
}

Le effect est nettoyé automatiquement quand le composant est détruit (si appelé dans un contexte d’injection).


3. Patterns courants avec signals

State partagé via service

@Injectable({ providedIn: "root" })
export class CartService {
  items = signal<CartItem[]>([]);
  count = computed(() => this.items().length);
  total = computed(() =>
    this.items().reduce((sum, i) => sum + i.prix * i.quantite, 0)
  );

  add(item: CartItem) {
    this.items.update(list => [...list, item]);
  }

  remove(id: string) {
    this.items.update(list => list.filter(i => i.id !== id));
  }

  clear() {
    this.items.set([]);
  }
}

Plusieurs composants peuvent injecter CartService et lire ses signals. Toute modification est propagée automatiquement.

Inputs avec signals

import { input } from "@angular/core";

@Component({
  selector: "app-card",
  standalone: true,
  template: `<div>{{ titre() }}</div>`,
})
export class CardComponent {
  titre = input.required<string>();
  variant = input<"default" | "primary">("default");
}

input() (Angular 17.1+) crée un signal en lecture seule. Plus moderne que @Input(). Permet d’utiliser computed() qui dépend des inputs sans recourir à ngOnChanges.

Outputs

import { output } from "@angular/core";

@Component(...)
export class CardComponent {
  clicked = output<string>();

  onClick() {
    this.clicked.emit("card-1");
  }
}

output() remplace @Output() new EventEmitter(). Plus simple, type-safe.

Two-way binding avec model

import { model } from "@angular/core";

@Component({
  selector: "app-input",
  template: `<input [value]="value()" (input)="value.set($event.target.value)" />`,
})
export class InputComponent {
  value = model.required<string>();
}

Permet [(value)]="userInput" au parent. Synchronise dans les deux sens automatiquement, ce qui simplifie sensiblement les composants de formulaire personnalisés en évitant le boilerplate des EventEmitter manuels.


4. RxJS : les opérateurs qui comptent vraiment

Pas besoin de connaître les 200 opérateurs RxJS. Une vingtaine couvrent 95% des cas.

Transformation

  • map(fn) : transforme chaque valeur
  • filter(predicate) : filtre les valeurs
  • tap(fn) : effet de bord sans modifier la valeur (pour debug, log)

Async control

  • switchMap(fn) : remplace par un nouvel Observable, annule le précédent. Idéal pour HTTP qui annule la requête précédente
  • mergeMap(fn) : exécute en parallèle, ne préserve pas l’ordre
  • concatMap(fn) : exécute séquentiellement, préserve l’ordre
  • exhaustMap(fn) : ignore les nouvelles émissions tant que la précédente n’est pas terminée

Temporel

  • debounceTime(ms) : émet uniquement après N ms d’inactivité
  • throttleTime(ms) : émet maximum une fois par N ms
  • delay(ms) : retarde toutes les émissions

Combinaison

  • combineLatest([a$, b$]) : émet quand au moins une source a émis et qu’elles ont toutes émis au moins une fois
  • forkJoin([a$, b$]) : équivalent Promise.all, attend toutes les sources, émet une seule fois
  • merge(a$, b$) : combine plusieurs sources

Filtre

  • distinctUntilChanged() : émet uniquement si la valeur change
  • take(n) : prend les N premières émissions puis complète
  • takeUntil(notifier$) : prend jusqu’à ce qu’une autre source émette

Erreurs

  • catchError(fn) : remplace une erreur par une valeur ou un autre Observable
  • retry(n) : retente N fois en cas d’erreur

Pattern d’usage typique

this.searchInput$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => this.api.search(query).pipe(
    catchError(() => of([])),
  )),
).subscribe(results => this.results.set(results));

Ce pattern (debounce + switchMap + catchError) est l’un des plus utiles. Difficile à reproduire élégamment sans RxJS.


5. HTTP avec RxJS

HttpClient retourne des Observables.

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

@Injectable({ providedIn: "root" })
export class ClientService {
  private http = inject(HttpClient);

  getClients() {
    return this.http.get<Client[]>("/api/clients");
  }

  searchClients(query: string) {
    return this.http.get<Client[]>("/api/clients", {
      params: { q: query },
    });
  }
}

Pourquoi des Observables et pas des Promises ?

  • Annulable : si on unsubscribe, la requête est annulée (utile avec switchMap)
  • Composable : on peut chaîner d’autres opérateurs (retry, catchError, transform)
  • Multicast possible avec share

Utilisation dans un composant

@Component({
  selector: "app-clients",
  standalone: true,
  imports: [CommonModule],
  template: `
    @if (clients()) {
      <ul>
        @for (c of clients()!; track c.id) {
          <li>{{ c.nom }}</li>
        }
      </ul>
    } @else {
      <p>Chargement...</p>
    }
  `,
})
export class ClientsComponent {
  private clientService = inject(ClientService);
  clients = toSignal(this.clientService.getClients());
}

toSignal convertit l’Observable en Signal directement utilisable dans le template.


6. Interop : toSignal et toObservable

import { toSignal, toObservable } from "@angular/core/rxjs-interop";

// Observable → Signal
const count$ = interval(1000);
const count = toSignal(count$, { initialValue: 0 });

// Signal → Observable
const value = signal(0);
const value$ = toObservable(value);

// Pattern combiné : signal → debounced HTTP → signal
const search = signal("");
const results = toSignal(
  toObservable(search).pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(q => this.api.search(q)),
  ),
  { initialValue: [] },
);

Ce pattern combine la simplicité des signals (state UI) avec la puissance de RxJS (flux asynchrone). Le résultat est un signal final consommable directement dans le template.

takeUntilDestroyed

import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

@Component(...)
export class MyComponent {
  ngOnInit() {
    this.someObservable$.pipe(
      takeUntilDestroyed(),
    ).subscribe(...);
  }
}

Désabonne automatiquement quand le composant est détruit. Remplace les patterns manuels avec Subject + takeUntil.


7. Pièges classiques

Mutation au lieu de remplacement

// Mauvais : Angular ne détecte pas le changement
const items = signal<Item[]>([]);
items().push(newItem);  // mutation, pas de notification

// Bon : nouvelle référence
items.update(list => [...list, newItem]);

Toujours utiliser set() ou update() avec une nouvelle référence.

Effects en cascade

const a = signal(0);
const b = signal(0);

effect(() => {
  if (a() > 5) b.set(a() * 2);  // change b dans un effect
});
// Possible boucle infinie si b déclenche un autre effect qui change a

Angular permet par défaut, mais rendre les changements explicites améliore la lisibilité. Pour calculer b en fonction de a : utiliser computed() plutôt qu’un effect.

Subscriptions oubliées

// Mauvais : subscription jamais nettoyée
this.observable$.subscribe(...);

// Bon
this.observable$.pipe(takeUntilDestroyed()).subscribe(...);
// ou
const sub = this.observable$.subscribe(...);
ngOnDestroy() { sub.unsubscribe(); }

switchMap vs mergeMap

// Mauvais : si l'utilisateur clique vite, plusieurs requêtes en parallèle, race conditions
this.click$.pipe(
  mergeMap(() => this.api.save()),
).subscribe();

// Bon : annule les requêtes précédentes
this.click$.pipe(
  switchMap(() => this.api.save()),
).subscribe();

// Mais : pour une opération qu'on ne veut PAS annuler (ex: paiement), exhaustMap
this.click$.pipe(
  exhaustMap(() => this.api.pay()),
).subscribe();

8. Performance et change detection

Avec provideZonelessChangeDetection(), Angular ne fait plus de change detection après chaque événement asynchrone, mais uniquement quand un signal change. Performances en hausse, surtout sur des UIs avec beaucoup de timers.

OnPush change detection

Pour les composants qui n’utilisent pas encore de signals :

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})

Le composant ne se re-rend que si ses inputs changent (référence) ou si on appelle manuellement markForCheck.

Signal-based avec OnPush

Combinaison optimale : composants avec signals + OnPush. Re-rendus uniquement quand les signals utilisés changent. C’est le défaut idéal pour 2026.

Voir Angular performance optimisation.


9. FAQ

Faut-il migrer tout RxJS vers signals ?

Non. RxJS reste meilleur pour les flux asynchrones. Migrer ce qui est synchrone (BehaviorSubject pour partager un état entre composants → signal). Garder RxJS pour HTTP, événements, flux complexes.

Subject est-il remplacé par signal ?

Pour partager une valeur entre composants : oui, signal est plus simple. Pour émettre des événements ponctuels (clic, message bus) : Subject reste pertinent. Pour un flux observable : Subject ou Observable.

Les inputs en signals sont-ils obligatoires ?

Non, les @Input() classiques fonctionnent encore. Mais input() (signal) est recommandé pour les nouveaux composants : meilleur typage, intégration avec computed(), pas besoin de ngOnChanges pour réagir aux changements.

Comment partager un signal entre composants frères ?

Via un service injecté dans les deux composants. Le service expose un signal. Les deux composants lisent et/ou écrivent. Plus simple qu’un système de communication parent-enfant à plusieurs niveaux.

Le mode zoneless est-il production-ready ?

Oui depuis Angular 18+. À combiner avec des composants entièrement signal-based pour profiter pleinement. Les composants legacy avec @Input classiques peuvent coexister mais ne bénéficient pas autant.

Comment debugger les rendus inattendus ?

Angular DevTools (extension Chrome/Firefox) montre les composants qui rerend, leurs inputs, leur state. Très utile pour identifier les rendus excessifs.

Quand forkJoin plutôt que combineLatest ?

forkJoin : on veut une seule émission, quand toutes les sources sont terminées (équivalent Promise.all). combineLatest : on veut une émission à chaque changement, en gardant la dernière valeur de chaque source. Pour des HTTP isolés : forkJoin. Pour des sources qui continuent d’émettre : combineLatest.


Articles liés (cluster Angular)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité