Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

Observable Angular : comprendre RxJS, async pipe et Signals

Un Observable est un objet RxJS qui représente une source de valeurs dans le temps. Il peut émettre une valeur, plusieurs valeurs, une erreur, puis se terminer.

Dans Angular, les Observables sont partout :

  • HttpClient retourne des Observables pour les requêtes HTTP ;
  • le Router expose des Observables pour les paramètres et les événements de navigation ;
  • les formulaires réactifs exposent valueChanges et statusChanges ;
  • RxJS permet de composer des flux avec des opérateurs comme map, filter, switchMap, catchError ou debounceTime.

Depuis l'arrivée des Signals, la question n'est plus "Observable ou Signal ?" mais plutôt : quelle abstraction utiliser à quel endroit ?

L'idée simple

Un Observable est comme une chaîne d'informations à laquelle on peut s'abonner. Tant que personne ne s'abonne, beaucoup d'Observables ne font rien. Quand une valeur arrive, l'Observable la pousse vers ses abonnés.

ts
import { Observable } from 'rxjs';

const messages$ = new Observable<string>((subscriber) => {
  subscriber.next('Bonjour');
  subscriber.next('Bienvenue dans Angular');
  subscriber.complete();
});

messages$.subscribe({
  next: (message) => console.log(message),
  error: (error) => console.error(error),
  complete: () => console.log('Terminé'),
});

Dans cet exemple :

  • messages$ est l'Observable ;
  • la fonction passée à subscribe() est l'abonnement ;
  • next reçoit les valeurs ;
  • error reçoit les erreurs ;
  • complete indique que le flux est terminé.

Pourquoi le signe $ ?

Le suffixe $ n'est pas obligatoire, mais il est très courant pour indiquer qu'une variable contient un Observable : users$, routeParams$, searchResults$.

Observable, Observer et Subscription

Trois mots reviennent souvent avec RxJS :

ConceptRôle
ObservableLa source de valeurs dans le temps
ObserverL'objet qui sait réagir à next, error et complete
SubscriptionLe lien actif entre l'Observable et l'abonné

Une souscription peut être arrêtée :

ts
import { interval } from 'rxjs';

const subscription = interval(1000).subscribe((value) => {
  console.log(value);
});

setTimeout(() => {
  subscription.unsubscribe();
}, 5000);

Ici, interval(1000) émet une valeur toutes les secondes. Après cinq secondes, unsubscribe() coupe l'abonnement.

Synchrone ou asynchrone ?

Un Observable peut être synchrone ou asynchrone.

ts
import { of, timer } from 'rxjs';

of(1, 2, 3).subscribe((value) => console.log(value));

timer(1000).subscribe(() => {
  console.log('Une seconde plus tard');
});

of(1, 2, 3) émet immédiatement. timer(1000) émet plus tard.

C'est une différence importante avec une Promise : une Promise représente une valeur future unique, alors qu'un Observable peut représenter zéro, une ou plusieurs valeurs.

Observable ou Promise ?

Utilisez une Promise quand vous attendez une seule valeur et que vous n'avez pas besoin d'annuler ou de composer un flux.

Utilisez un Observable quand :

  • il peut y avoir plusieurs valeurs dans le temps ;
  • vous voulez annuler l'écoute ;
  • vous voulez transformer, filtrer ou combiner des événements ;
  • vous travaillez avec les API Angular qui retournent déjà des Observables.
ts
import { fromEvent } from 'rxjs';

const clicks$ = fromEvent(document, 'click');

clicks$.subscribe(() => {
  console.log('Clic détecté');
});

Un clic peut arriver plusieurs fois. Un Observable est donc plus naturel qu'une Promise pour ce cas.

Le cas le plus courant : HttpClient

Avec Angular, HttpClient retourne un Observable.

ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';

export interface User {
  id: number;
  name: string;
}

@Injectable({ providedIn: 'root' })
export class UsersService {
  private readonly http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users');
  }
}

Dans un composant, évitez de souscrire juste pour copier la donnée dans une propriété quand le template peut lire directement l'Observable.

ts
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { UsersService } from './users.service';

@Component({
  selector: 'app-users',
  imports: [AsyncPipe],
  template: `
    @for (user of users$ | async; track user.id) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class UsersComponent {
  private readonly usersService = inject(UsersService);

  readonly users$ = this.usersService.getUsers();
}

Le pipe async s'abonne, renvoie la dernière valeur reçue, déclenche la détection de changement, puis se désabonne automatiquement quand le composant est détruit. Il se désabonne aussi de l'ancien Observable si la référence change.

Quand faut-il se désabonner ?

Tout dépend de la manière dont l'Observable est consommé.

SituationQue faire ?
Observable lu dans le templateUtiliser le pipe async
Observable converti en Signal avec toSignal()Angular nettoie automatiquement dans le contexte d'injection
subscribe() manuel dans un composantUtiliser takeUntilDestroyed()
Observable qui se termine seul, comme une requête HTTPLe risque de fuite est plus faible, mais le pipe async ou toSignal() restent plus lisibles
Flux long, comme WebSocket, interval, événements DOMPrévoir un nettoyage explicite

Exemple avec takeUntilDestroyed() :

ts
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { fromEvent } from 'rxjs';

@Component({
  selector: 'app-tracker',
  template: `Position suivie`,
})
export class TrackerComponent {
  constructor() {
    fromEvent<MouseEvent>(document, 'mousemove')
      .pipe(takeUntilDestroyed())
      .subscribe((event) => {
        console.log(event.clientX, event.clientY);
      });
  }
}

takeUntilDestroyed() est utile quand vous devez vraiment appeler subscribe() vous-même.

Observable ou Signal ?

Un Signal représente une valeur actuelle, lisible de façon synchrone avec user(). Un Observable représente un flux de valeurs dans le temps.

En pratique :

  • gardez les Observables pour les événements, les requêtes, les WebSockets, les formulaires, le Router et les compositions RxJS ;
  • utilisez les Signals pour l'état local d'un composant ou d'un service ;
  • convertissez avec toSignal() quand un flux RxJS doit être lu facilement dans le template ou dans un computed().
ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UsersService } from './users.service';

@Component({
  selector: 'app-users-count',
  template: `<p>{{ users().length }} utilisateurs</p>`,
})
export class UsersCountComponent {
  private readonly usersService = inject(UsersService);

  readonly users = toSignal(this.usersService.getUsers(), {
    initialValue: [],
  });
}

toSignal() crée une souscription immédiatement. Il faut donc éviter de l'appeler plusieurs fois pour le même Observable. Créez le Signal une fois, puis réutilisez-le.

Convertir un Signal en Observable

L'autre sens existe aussi avec toObservable(). C'est utile quand une valeur en Signal doit entrer dans une chaîne RxJS.

ts
import { Component, inject, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';

interface SearchResult {
  id: number;
  title: string;
}

@Component({
  selector: 'app-search',
  template: `
    <input
      [value]="query()"
      (input)="query.set($any($event.target).value)"
    />
  `,
})
export class SearchComponent {
  private readonly http = inject(HttpClient);

  readonly query = signal('');

  readonly results$ = toObservable(this.query).pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((query) =>
      this.http.get<SearchResult[]>('/api/search', {
        params: { q: query },
      }),
    ),
  );
}

Ici, le Signal garde la valeur du champ. RxJS gère le délai, évite les requêtes identiques, puis annule la requête précédente avec switchMap quand une nouvelle recherche arrive.

Subject et BehaviorSubject

Un Subject est à la fois un Observable et un Observer. On peut lui envoyer une valeur avec next(), puis exposer le flux aux consommateurs.

ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CounterStore {
  private readonly countSubject = new BehaviorSubject(0);

  readonly count$ = this.countSubject.asObservable();

  increment(): void {
    this.countSubject.next(this.countSubject.value + 1);
  }
}

BehaviorSubject garde une valeur courante. C'est pratique pour un petit état partagé, mais ce n'est pas toujours le meilleur choix pour l'état local d'un composant : un signal() est souvent plus simple.

Les erreurs courantes

1. S'abonner dans un composant pour remplir une propriété

ts
// A eviter quand le template peut consommer l'Observable directement.
this.usersService.getUsers().subscribe((users) => {
  this.users = users;
});

Préférez users$ | async ou toSignal().

2. Oublier que subscribe() déclenche le travail

Certains Observables sont froids : chaque abonnement relance leur logique. Avec HttpClient, cela peut déclencher une nouvelle requête. Si vous avez besoin de partager le résultat, regardez du côté de shareReplay ou stockez la donnée dans un service.

3. Transformer un Observable en Signal trop souvent

ts
// A eviter : cree une nouvelle souscription a chaque appel.
get users() {
  return toSignal(this.usersService.getUsers(), { initialValue: [] });
}

Créez le Signal une seule fois :

ts
readonly users = toSignal(this.usersService.getUsers(), {
  initialValue: [],
});

4. Utiliser RxJS pour tout

RxJS est puissant, mais ce n'est pas une obligation pour chaque état. Pour une valeur locale comme isOpen, selectedTab ou searchText, un Signal est souvent plus direct.

Tableau de décision

BesoinChoix naturel
Une valeur locale modifiable dans un composantsignal()
Une valeur calculée depuis d'autres valeurscomputed()
Une requête HTTP AngularObservable retourne par HttpClient, puis async ou toSignal()
Des événements répétésObservable
Une recherche avec annulationtoObservable(signal).pipe(switchMap(...))
Un petit état partagé historiqueBehaviorSubject ou Signal dans un service
Une seule valeur future hors AngularPromise

A retenir

Un Observable sert à représenter un flux de valeurs dans le temps. Dans Angular, il reste essentiel pour HttpClient, le Router, les formulaires, les événements et les compositions RxJS.

Les Signals ne remplacent pas RxJS partout. Ils complètent le modèle : Signals pour l'état courant, Observables pour les flux.

Le bon réflexe est donc :

  • lire les Observables dans le template avec le pipe async ;
  • utiliser toSignal() quand vous voulez une valeur synchrone dans le composant ;
  • utiliser takeUntilDestroyed() quand vous avez besoin d'un subscribe() manuel ;
  • garder RxJS pour les cas où les opérateurs comme switchMap, debounceTime ou catchError apportent une vraie valeur.

Sources