Makina Blog

Le blog Makina-corpus

Mise en pratique de RxJS dans Angular


Les quelques bases suffisantes pour bien utiliser RxJS dans Angular. Cet article a été écrit avec Angular 6 et RxJS 6. Il constitue une bonne introduction à la programmation réactive avec Angular.

Tout comme les promesses avaient modifié nos habitudes, la programmation réactive nous oblige à apprendre un nouveau paradigme. Les documentations sont dures à comprendre, les schémas font peur et on ne sait pas par où commencer.

Et pourtant, il suffit de connaitre quelques bases pour s'en sortir dans presque tous les cas. Nous vous présenterons donc dans cet article ces quelques classes et méthodes qui vous suivront tout au long de vos développements d'applications Angular.

Attention : RxJS est une librairie qui évolue beaucoup, plusieurs syntaxes ont changé avec les version 5.5 et 6 de RxJS, cet article a été mis à jour en conséquence, se reporter au bas de l'article pour plus d'informations sur les ajustements nécessaires.

Qu'est ce que la programmation réactive ?

La programmation réactive se base sur le concept d'observateur. Si vous n'êtes pas familier avec ce principe, le principe est tout simplement que l'on définit des observables et des observateurs. Les observables vont émettre des événements qui seront interceptés par les observateurs.

La programmation réactive va étendre ce concept en permettant de combiner les observables, modifier les événements à la volée, les filtrer, etc.

Observable

Souscrire à des observables

Observable est l'objet de base de la programmation réactive. C'est lui qui va nous permettre de créer des (vous l'aurez surement deviné) observables.

import { of } from 'rxjs';

const myObservable = Observable.of(42);

of est la méthode la plus simple et permet de créer un observable n'envoyant qu'une seule valeur (42 dans notre exemple, mais cela peut tout aussi bien être un objet ou une chaîne). Nous avons donc un observable mais personne pour le surveiller. C'est ce que la méthode subscribe va nous permettre de faire en créant nos observateurs.

const myObservable = Observable.of(42);

myObservable.subscribe((value) => { console.log(value); });

subscribe prend en paramètre l'observateur, qui est une simple fonction qui recevra les valeurs émises par l'observable. Notre console affichera donc 42 dans notre exemple.

subscribe peut également prendre deux autres arguments : une fonction appelée en cas d'erreur, et une autre appelée une fois l'observable fini (un observable fini n'enverra plus de données).

const myObservable = Observable.of(42);

myObservable.subscribe((value) => {
    console.log(value);
}, (error) => {
    console.log(error);
}, () => {
    console.log('Fini !');
});

Il existe d'autres manière de créer des observateurs à partir de différentes sources de données :

import { from, interval } from 'rxjs';

// A partir d'un tableau
const myObservable2 = from(['bonjour', 'le', 'monde']);


// Envoie une valeur toutes les 2 secondes
const myObservable3 = interval(2000);

Modifier et filtrer les données à la volée

La méthode map d'Observable fonctionne de la même manière que la méthode du même nom sur les tableaux : elle prend des valeurs en entrée, les transforme et les renvoie en sortie. Exemple :

const myObservable = of("foobar");

myObservable.pipe(
    tap((value) => console.log('Avant : ' + value)),
    map((value: string) => value.length),
    tap((value) => console.log('Après : ' + value)),
).subscribe((value: number) => {
    console.log(value);  // 6
});

Application aux requêtes HTTP

C'est bien beau tout ça, mais ça n'est toujours que de la théorie… Un cas concret donc ! Et ça tombe bien, car les observables sont largement utilisés dans Angular, comme par exemple :

  • par le module HttpClient.
  • par le module de gestion des routes, lorsque l'URL change
  • dans la gestion des formulaires, lorsque l'utilisateur saisit des valeurs dans des champs de saisie

Nous allons illustrer cet article par des exemples liés à la geston des requêtes HTTP. Les différentes méthodes du service http retournent des Observable<any>. Notez que le type des variables émises par l'observable est précisé entre chevrons, les observables sont en effet génériques.

Maintenant que vous avez compris tout ça, rien de plus simple que de faire des requêtes. Et parce qu'on aime bien coder proprement, nos requêtes doivent être dans un service particulier. Imaginons qu'on veuille récupérer une liste d'articles présents sur notre site.

Les données seront en JSON sous la forme suivante :

[
    {
      "author": "John Doe",
      "title": "first article",
      "body": "A very interesting article",
      "published": "2017-07-09T09:16:35Z"
    },
    {
      "author": "Another Author",
      "title": "another article",
      "body": "Lorem ipsum etc",
      "published": "2018-08-07T14:00:00Z"
    },
    {
      "author": "John Doe",
      "title": "second article",
      "body": "Another very interesting article",
      "published": "2017-07-12T13:45:27Z"
    }
]

Nous allons commencer par définir une classe JS qui peut être plus ou moins proche de la structure retournée par l'API.

export class Article {
    public static fromJson(json: Object): Article {
        return new Article(
            json['author'],
            json['title'],
            json['body'],
            new Date(json['published'])
        );
    }

    constructor(public author: string,
                public title: string,
                public body: string,
                public published: Date) {
    }
}

Noter que plusieurs exemples simples sont disponibles sur Internet, et se contentent de manipuler des object JS à l'exécution, une interface étant définie pour profiter du typage statique de Typescript. Nous préférons passer par des classes, ce qui nous permet de gérer le traitement de champs complexes comme des dates qui n'ont pas d'équivalence simple comme des chaînes ou des nombres, ou bien de gérer des structures de données potentiellement différentes de ce qui est retourné par l'API.

Nous pouvons ensuite exploiter cette classe dans le service d'accès aux données et le composant associé :

@Injectable()
export class DataService {
  constructor(protected http: HttpClient) {}

  public getArticles(): Observable<Article[]> {
    return this.http.get('http://localhost:3000/articles/').pipe(
      map(
        (jsonArray: Object[]) => jsonArray.map(jsonItem => Article.fromJson(jsonItem))
      )
    );
  }
}


@Component({
  selector: 'app-articles-list',
  templateUrl: './articles-list.component.html',
  styleUrls: ['./articles-list.component.scss']
})
export class ArticleListComponent {

  public articles: Article[];

  constructor(private data: DataService) {
  }

  ngOnInit() {
    this.data.getArticles().subscribe(
      articles => this.articles = articles
    );
  }
}

Le service s'occupe de la création des Article, et le composant n'a presque rien à faire. Et cela nous permet surtout de réutiliser facilement cet observable, c'est le grand avantage de la programmation réactive. Par exemple si en fait on ne veut que les articles de John Doe (grâce à filter, assez explicite par lui-même) :

this.data.getArticles().pipe(
  map(
    (articles: Article[]) => articles.filter(
      (article: Article) => article.author === 'John Doe'
    )
  )
).subscribe(
  (articles: Article[]) => {
    this.articles = articles;
  }
);

Les méthodes comme map et filter, qui permettent de créer un observable à partir d'un autre observable, sont appelées en RxJS, opérateurs.

Attention : les fonctions map et filter existent en 2 versions qui s'appliquent sur les tableaux JS et en tant qu'opérateurs sur les observables. Dans l'exemple ci dessus, on utile l'opérateur map sur un observable, mais filter est bien la fonction JS bien connue appliquée à un tableau.

Nous allons voir maintenant que certains permettent de combiner des observables entre-eux.

Combiner les observables

Il existe des opérateurs qui permettent de créer un observable à partir de plusieurs observables. Ils ont des noms particulièrement barbares, mais sont pour certains d'entre eux fréquemment utiles. Nous allons vous présenter les trois plus courants dans la programmation d'interfaces web.

forkJoin: combiner la sortie de plusieurs observables

Imaginons que vous souhaitiez afficher la liste des auteurs filtrée suivant un critère qui provient d'une autre requête http: celle qui renvoie l'auteur du mois par exemple. Ce que nous aimerions pouvoir faire, c'est :

  • lancer la requête permettant de récupérer l'auteur du mois,
  • en même temps lancer la requête permettant d'avoir la liste des articles,
  • et une fois qu'on a les deux retours, combiner les résultats pour sortir la bonne valeur.

C'est rendu facile avec l'opérateur forkJoin. Regardons l'implémentation suivante :

// Dans notre DataService

getAuthorOfTheMonth(): Observable<Object> {
  return this.http.get<Object>('http://localhost:3000/authorofthemonth');
}

getAuthorOfTheMonthArticles(): Observable<Article[]> {
  return forkJoin(
    this.getArticles(),         // la requête http qui récupère la liste des articles
    this.getAuthorOfTheMonth()  // celle qui récupère l'auteur du mois
  ).pipe(
    map(([articles, authorOfTheMonth]) =>
      // le filtre qui exclut les articles qui ne sont pas de cet auteur
      articles.filter(article => article['author'] === authorOfTheMonth['name'])
    )
  )
}

forkJoin prend en paramètre plusieurs observables, et résulte en un observable qui prend en entrée la sortie de tous les autres, sous la forme d'une liste. Dans notre exemple, nous faisons suivre le forkJoin d'une opération map qui utilise les informations provenant des deux sorties. Dans votre composant, vous pouvez souscrire à cet observable et appelant la méthode getAuthorOfTheMonthArticles()

combineLatest: écouter un groupe d'observables

Supposons maintenant que nous souhaitions afficher la liste des articles de l'utilisateur connecté, sachant que cet utilisateur peut être anonyme, se connecter, se déconnecter… C'est le type de use case qui est très facile à implémenter en programmation web classique sans ajax, mais qui peut devenir rapidement infernal en single page application (SPA).

RxJs simplifie la résolution de ce genre de choses, grâce notamment à l'opérateur combineLatest.

L'opérateur combineLatest est similaire au forkJoin à une distinction près. Il ne se termine pas une fois qu'il a reçu les résultats de chaque observable, mais continue d'écouter, et émet de nouveau à chaque fois qu'un des observables source a émis de nouveau. Il réémet alors la liste de toutes les dernières valeurs émises par chaque observable.

Supposons qu'on a une méthode getAuthenticatedUser() qui renvoit un observable qui émet un objet représentant l'utilisateur à chaque fois qu'il change. Nous pourrons écrire la méthode suivante pour récupérer une liste toujours à jour :

// Dans notre DataService

getAuthenticatedUserArticles(): Observable<Article[]> {
  return combineLatest(
    this.getAuthenticatedUser(),  // l'observable qui émet l'utilisateur connecté
    this.getArticles()   // l'observable qui émet la liste des articles
  ).pipe(
    map(([user, articles]) => {
      if (user.isAnonymous) {
        return [];
      }
      return articles.filter(article => article['author'] === user['name'])
    })
  )
}

À chaque fois que l'utilisateur authentifié va changer, l'observable renvoyé par getAuthenticatedUser() va émettre une nouvelle valeur. Si vous avez subscribe à cet observable dans votre composant pour mettre à jour le contenu de la liste à l'écran, celle-ci va se mettre à jour à chaque fois que l'utilisateur se connecte ou se déconnecte !

Notez que :

  • la requête qui récupère la liste des articles ne sera exécutée ici qu'une seule fois
  • l'observable getAuthenticatedUser() capable d'émettre plusieurs fois sera sans doute un BehaviorSubject dont nous parlons dans la suite de l'article.

mergeMap: utiliser la sortie d'un observable en entrée d'un autre

Supposons que notre api a un point d'entrée pour renvoyer la liste des articles d'un seul auteur. On pourrait alors améliorer la façon de récupérer les articles de l'auteur du mois :

  • d'abord on récupère l'auteur du mois,
  • puis on demande la liste des articles pour cet auteur.

On aura toujours besoin de deux observables, mais cette fois les traitements seront successifs au lieu d'être simultanés. L'opérateur mergeMap nous permet de faire cela facilement.

// Dans notre DataService

getAuthorOfTheMonthArticles(): Observable<Article[]> {
  return this.getAuthorOfTheMonth().pipe(
    mergeMap((authorOfTheMonth) => {
      return this.http.get<Article[]>('http://localhost:3000/articles/?author=' + authorOfTheMonth['name'])
    })
  )
}

mergeMap crée un observable à partir d'un observable source (ici, la requête de l'auteur du mois) qui envoie sa sortie en entrée d'un autre observable (ici, la requête des articles pour cet auteur).

Note: si certains observables de la chaîne sont répétables et que l'ordre de sortie compte, alors intéressez-vous à l'opérateur concatMap qui pourrait mieux correspondre à votre besoin : <https://fernandocejas.com/2015/01/11/rxjava-observable-tranformation-concatmap-vs-flatmap/>

Avec ces trois opérateurs, vous maîtrisez les trois manières les plus utiles de combiner des observables dans une application Angular.

Subject

Un Subject est à la fois un observable ET un observateur. On peut donc subscribe dessus, mais également lui envoyer des valeurs :

const subject = new Subject<number>();

subject.subscribe((number) => {
    console.log(1, number);
});


subject.subscribe((number) => {
    console.log(2, number);
});


subject.next(1);  // On envoie une donnée
subject.next(2);  // On envoie une autre donnée
subject.complete();  // On indique que l'observable n'enverra plus de données

Subject à lui seul ne sert pas forcément à grand chose, mais ses classes spécialisées sont, elles, bien utiles.

BehaviorSubject

Un BehaviorSubject a obligatoirement une valeur par défaut. Il sauvegarde la dernière valeur qu'il a émis et l'envoie aux observateurs lors de leur subscribe (si vous ne l'aviez pas remarqué, un observateur ne récupère pas les événements passés mais uniquement les nouveaux). Exemple d'utilisation avec un système permettant de gérer l'utilisateur courant :

/* A user of the app */
export interface User {
  userName: string;
  isAnonymous: boolean;
}

const ANONYMOUS_USER = <User>{
  isAnonymous: true,
  userName: 'Anonymous User'
};

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  protected authenticatedUser: BehaviorSubject<User> = new BehaviorSubject<User>(ANONYMOUS_USER);

  constructor(private api: ApiService) {
    // api est un service permettant de faire des appels à notre API REST
  }

  public login(userName: string, password: string): Observable<User> {
    const service = this;
    return service.api.post('/accounts/login/', {
      'username': userName,
      'password': password
    }).pipe(
      map(userJson => {
        const newUser = <User>({
          name: userJson['name'],
          isAnonymous: false,
        });
        service.authenticatedUser.next(newUser);
        return newUser;
      })
    );
  }

  public logout(): Observable<Response> {
    const service = this;
    return service.api.post('/accounts/logout/', {})
      .pipe(
        map((response: Response) => {
          service.authenticatedUser.next(ANONYMOUS_USER);
          return response;
        })
      );
  }

  public getAuthenticatedUser(): Observable<User> {
    return this.authenticatedUser;
  }
}

L'utilisateur est donc anonyme tant qu'il ne se connecte pas. Si on subscribe à getAuthenticatedUser(), on pourra exécuter du code avec cette valeur par défaut, puis à chaque fois que l'utilisateur courant va changer. On est ainsi assurés de toujours travailler avec des valeurs à jour.

AsyncSubject

AsyncSubject ne retient que sa dernière valeur, et ne l'envoie que lorsqu'il est fini. Il peut être pratique pour mettre en cache une opération qui ne sera effectuée qu'une fois :

protected vocabulary: AsyncSubject<object> = new AsyncSubject();

constructor(private api: HTTPService) {
  this.api.get('/vocabulary/').pipe(
    map((jsonResponse: object) => {
      this.vocabulary.next(jsonResponse);
      this.vocabulary.complete();
    })
  );
}

public getVocabulary(): Observable<object> {
  return this.vocabulary;
}

Cette présentation de RxJS n'était bien sûr pas exhaustive, mais elle vous permettra déjà d'aller assez loin dans vos applications Angular sans vous prendre la tête.

Evolution de l'API RxJS

Il y a eu de nombreux entre les versions 5 et 5.5 puis en 5.5 et 6.

Les principaux changements sont d'une part l'introduction des pipeable operators en version 5.5. Ainsi cet ancien code :

range(1, 200)
    .filter(x => x % 2 === 1)
    .do(console.log('Do something'))
    .map(x => {
            console.log(`Processing x=${x}`);
            return x + x;
        }
    )
    .subscribe(x => console.log(x));

devient :

import { range } from 'rxjs';
import { filter, map, tap } from 'rxjs/operators';

range(1, 200)
  .pipe(
      filter(x => x % 2 === 1),
      tap(x => console.log(x)),
      // map(x => x + x)
       map(x => {
           console.log(`Processing x=${x}`);
           return x + x;
       })
  )
  .subscribe(x => console.log(x));

Il y a également eu plusieurs changements dans la syntaxe des imports, visant à ne charger que le code des opérateurs réellement utilisés sur son projet ("tree shaking").

Il est possible de visualiser tous ces changement en allant sur http://reactive.how/rxjs/explorer et en basculant d'une version à l'autre de RxJS.

Pour plus de détails sur ces changements, se reporter à ces notes de version : https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/v6/migration.md

Pour aller plus loin

  • Un formidable article, qui est la présentations la plus claire jamais faite de la différence entre concat, switch, merge et exhaust, et qui explique très bien le fonctionnement des observables, en insistant notamment sur la notion de "complétion" (peu abordée ici mais cruciale pour "aller plus loin"): https://blog.angular-university.io/rxjs-higher-order-mapping/
  • Le site de la programmation réactive, contenant la théorie ainsi que des exemples dans tous les languages : http://reactivex.io/
  • Le dépôt de RxJS : https://github.com/ReactiveX/rxjs
  • Chez Makina Corpus, nous travaillons beaucoup avec RxJS sur des projets Angular. Si vous souhaitez passer à la programmation réactive, nous pouvons vous aider. Contactez-nous ! contact@makina-corpus.com

Sachez aussi que notre formation Angular comprend une initiation aux observables.

Formations associées

Formations Front end

Formation Angular

Nantes Du 22 au 24 mai 2024

Voir la formation

Formations Front end

Formation Angular avancé

Toulouse Du 29 au 31 mai 2024

Voir la formation

Actualités en lien

Image
Capture d'une partie de carte montrant un réseau de voies sur un fond de carte sombre. Au centre, une popup affiche les information de l'un des tronçons du réseau.
28/02/2024

Géné­rer un fichier PMTiles avec Tippe­ca­noe

Exemple de géné­ra­tion et d’af­fi­chage d’un jeu de tuiles vecto­rielles en PMTiles à partir de données publiques.

Voir l'article
Image
Read The Docs
01/02/2024

Publier une documentation VitePress sur Read The Docs

À l'origine, le site de documentation Read The Docs n'acceptait que les documentations Sphinx ou MKDocs. Depuis peu, le site laisse les mains libres pour builder sa documentation avec l'outil de son choix. Voici un exemple avec VitePress.

Voir l'article
Image
Widget
04/04/2023

Créer une application en tant que composant web avec Stencil

Mise en place dans le cadre de Geotrek, cette solution permet de se passer d'une iFrame pour afficher une application dans n'importe quel site.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus