RxJS

Quizz : Concepts et cas d'usage avancés

Laurent Wroblewski

@LaurentWrob

Philippe Beroucry

@philou_philou_p

Groupe HN

C'est quoi RxJS?

. Implémentation de Rx en Javascript

. Programmation reactive

. Facilite la gestion de l'asynchronisme (no callback hell)

. Fortement utilisé par des frameworks & libs populaires comme Angular

 

Ca existe déjà en JS non?

Pourquoi RxJS?

. Encore trop peu connu!

Euh Rx, c'est les http get, c'est ça?

. Très puissant et trop sous utilisé.

Moi, les Promises me suffisent!

Pourquoi RxJS?

. Favorise la programmation fonctionnelle.

this.products$: Product[] = fromEvent(input, 'keyup')
    .pipe(
        .map((event) => event.target.value)
        .filter((search: String) => search.length > 2)
        .debounceTime(250)
        .distinctUntilChanged()
        ...
    );

. Pattern observable universel :

RxJava, RxKotlin, RxDart, .NET RX, etc...

Are you ready???

Question 0

. Aujourd'hui nous parlons d'une technologie en particulier...

. Question : laquelle?

Question 0

. Réponse : RxJS, évidemment...

Qui a fait une erreur... ?

Plus sérieusement...

Question 1 : Transformations

. Philou doit développer un raccourci vers le panier utilisateur sur son site de commerce.

. Celui-ci doit présenter en continu le nombre de produits choisis par l'utilisateur.

Question 1 : Transformations

. Pour ce faire, Philou souhaite consommer la donnée currentUser.cart.numProducts :

 

const cartMessage$: Observable<String> = currentUser$.pipe(
    XXX
    distinctUntilChanged(),
    map((numProducts) => numProducts === 0 ?
        this.trad.text('empty_cart'), 
        this.trad.text('cart_product', [numProducts]))
);

Question 1 : Transformations

. Question : quel(s) opérateur(s) utiliser pour récupérer le nombre de produits?

 

const cartMessage$: Observable<String> = currentUser$.pipe(
    XXX
    distinctUntilChanged(),
    map((numProducts) => numProducts === 0 ?
        this.trad.text('empty_cart'), 
        this.trad.text('cart_product', [numProducts]))
);

Question 1 : Transformations

. Réponses :

  map(({ cart }) => cart.numProducts)

  pluck('cart', 'numProducts')

 

const cartMessage$: Observable<String> = currentUser$.pipe(
    XXX
    distinctUntilChanged(),
    map((numProducts) => numProducts === 0 ?
        this.trad.text('empty_cart'), 
        this.trad.text('cart_product', [numProducts]))
);

Question 2 : Combinaisons

. Laurent décide d’investir une partie de son épargne en bourse...

. Il choisit Total, BPCE, Renault et Google. Des rumeurs annoncent un krach boursier imminent...

Question 2 : Combinaisons

. race fait partie de la famille des opérateurs de comparaison.

. Question : Quel est le résultat du code suivant ?

const sourceRace = race(
  interval(1500).pipe(mapTo('total')),
  interval(2000).pipe(mapTo('bpce')),
  interval(1500).pipe(mapTo('renault')),
  interval(1990).pipe(mapTo('google')),
  of('krach boursier'));
const subscribe = sourceRace.pipe(take(1))
  .subscribe(val => console.log(val));

Question 2 : Combinaisons

. Solution : krach boursier

. of est completed immédiatement.

const sourceRace = race(
  interval(1500).pipe(mapTo('total')),
  interval(2000).pipe(mapTo('bpce')),
  interval(1500).pipe(mapTo('renault')),
  interval(1990).pipe(mapTo('google')),
  of('krach boursier'));
const subscribe = sourceRace.pipe(take(1))
  .subscribe(val => console.log(val));

// krach boursier

Question 2 : Combinaisons

. Et dans ce cas?

const sourceRace = race(
  interval(1500).pipe(mapTo('total')),
  interval(2000).pipe(mapTo('bpce')),
  interval(1500).pipe(mapTo('renault')),
  interval(1990).pipe(mapTo('google')),
  of('krach boursier').pipe(
    filter(x => x === 'tout va bien')
  ));
const subscribe = sourceRace.pipe(take(1))
  .subscribe(val => console.log(val));

Question 3 : Filtres

. Laurent, pour se consoler de ses placements douteux, décide de se rendre sur son site e-commerce préféré pour se faire une petite folie.

. Pour maintenir sa session active, le site e-commerce doit relever si l’utilisateur est toujours actif, et ce toutes les deux secondes.

Question 3 : Filtres

. Le code suivant permet de détecter si un utilisateur est actif sur une page.

. On souhaite détecter son activité tous les deux secondes.

. Question : Quel opérateur permettrait de n'avoir que le dernier event déclenché?

   merge(
      fromEvent(document, 'mousemove'),
      fromEvent(document, 'keydown'),
      fromEvent(document, 'keypress'),
      fromEvent(document, 'wheel'),
      fromEvent(document, 'touchstart'),
      fromEvent(document, 'touchend'),
      fromEvent(document, 'touchmove'),
    ).pipe(
      XXX
    ).subscribe((x) => console.log(x));

Question 3 : Filtres

. Réponse : throttleTime

Question 3 : Filtres

. debounceTime : garde la valeur la plus récente émise.

Question 4 : Combinaisons

. C'est l'urgence pour Philou, la loi RGPD vient de passer, et il faut modifier toutes les applications en production.

Question 4 : Combinaisons

ngOnInit() {
  this.user$ = XXX(
    this.http.get('https://api/user/' + this.userId),
    this.http.get('https://api/user-cart/' + this.userId)
  ).pipe(
    map(([user, cart]) => ({ ...user, cart }))
  );
  
  this.previousDoNotCall$ = this.user$.pipe(
    map((user) => user.doNotCall)
  );
  
  this.contactData$ = XXX(
    this.inputValueChanges('mobilePhone'),
    this.inputValueChanges('doNotCall'),
    this.inputValueChanges('mail'),
    this.previousDoNotCall$
  ).pipe(
    debounceTime(250),
    distinctUntilChanged(),
    map(([mobilePhone, doNotCall, mail, previousDoNotCall]) =>
	  ({ mobilePhone, doNotCall, mail, hasChanged: doNotCall !== previousDoNotCall }))
  );
}

Ouf

Question 4 : Combinaisons

. Question : Quels opérateurs utiliser?

ngOnInit() {
  this.user$ = XXX(
    this.http.get('https://api/user/' + this.userId),
    this.http.get('https://api/user-cart/' + this.userId)
  ).pipe(
    map(([user, cart]) => ({ ...user, cart }))
  );
  
  this.previousDoNotCall$ = this.user$.pipe(
    map((user) => user.doNotCall)
  );
  
  this.contactData$ = XXX(
    this.inputValueChanges('mobilePhone'),
    this.inputValueChanges('doNotCall'),
    this.inputValueChanges('mail'),
    this.previousDoNotCall$
  ).pipe(
    debounceTime(250),
    distinctUntilChanged(),
    map(([mobilePhone, doNotCall, mail, previousDoNotCall]) =>
	  ({ mobilePhone, doNotCall, mail, hasChanged: doNotCall !== previousDoNotCall }))
  );
}

Question 4 : Combinaisons

. Solution : Quels opérateurs utiliser?

ngOnInit() {
  this.user$ = forkJoin(
    this.http.get('https://api/user/' + this.userId),
    this.http.get('https://api/user-cart/' + this.userId)
  ).pipe(
    map(([user, cart]) => ({ ...user, cart }))
  );
  
  this.previousDoNotCall$ = this.user$.pipe(
    map((user) => user.doNotCall)
  );
  
  this.contactData$ = combineLatest(
    this.inputValueChanges('mobilePhone'),
    this.inputValueChanges('doNotCall'),
    this.inputValueChanges('mail'),
    this.previousDoNotCall$
  ).pipe(
    debounceTime(250),
    distinctUntilChanged(),
    map(([mobilePhone, doNotCall, mail, previousDoNotCall]) =>
	  ({ mobilePhone, doNotCall, mail, hasChanged: doNotCall !== previousDoNotCall }))
  );
}

Question 4 : Combinaisons

. forkJoin :

Question 4 : Combinaisons

. combineLatest :

combineLatest

Question 4 : Combinaisons

. combineLatest :

combineLatest zip

Question 4 : Combinaisons

. combineLatest :

Question 5 : Transformations

. Philou doit développer une recherche par auto-complétion sur une liste de produits.

Question 5 : Transformations

. Philou doit développer une recherche par auto-complétion sur une liste de produits.

const searchText$: Observable<string> = 
      fromEvent(this.input.nativeElement, 'keyup')
    .pipe(
        map(event => event.target.value),
        startWith(''),
        debounceTime(250),
        distinctUntilChanged()
    ); 

const products$: Observable<Product[]> = searchText$
    .pipe(
        XXX(search => this.fetchProducts(search))        
    )
    .subscribe();

function fetchProducts(search:string): Observable<Product[]> {
    const params = new HttpParams().set('search', search); 
    return this.http.get(`/api/products/`, {params});
}

Question 5 : Transformations

. Question : Quel est l'opérateur le plus optimisé dans ce cas?

const searchText$: Observable<string> = 
      fromEvent(this.input.nativeElement, 'keyup')
    .pipe(
        map(event => event.target.value),
        startWith(''),
        debounceTime(250),
        distinctUntilChanged()
    ); 

const products$: Observable<Product[]> = searchText$
    .pipe(
        XXX(search => this.fetchProducts(search))        
    )
    .subscribe();

function fetchProducts(search:string): Observable<Product[]> {
    const params = new HttpParams().set('search', search); 
    return this.http.get(`/api/products/`, {params});
}

Question 5 : Transformations

. Réponse: switchMap

const searchText$: Observable<string> = 
      fromEvent(this.input.nativeElement, 'keyup')
    .pipe(
        map(event => event.target.value),
        startWith(''),
        debounceTime(250),
        distinctUntilChanged()
    ); 

const products$: Observable<Product[]> = searchText$
    .pipe(
        switchMap(search => this.fetchProducts(search))        
    )
    .subscribe();

function fetchProducts(search:string): Observable<Product[]> {
    const params = new HttpParams().set('search', search); 
    return this.http.get(`/api/products/`, {params});
}

Question 5 : Transformations

. Réponse: switchMap

Question 6 : Transformations

. Philou doit maintenant s'occuper du formulaire de login.

Question 6 : Transformations

. Les architectes lui demandent d'ailleurs d'optimiser son code, car le login côté serveur dure 30 bonnes secondes...

Ce ne serait pas plus un problème back?

Question 6 : Transformations

. L'implémentation de Philou :

fromEvent(this.saveButton.nativeElement, 'click')
    .pipe(
        XXX(() => this.fetchProducts(this.form.value))
    )
    .subscribe();

Question 6 : Transformations

. Question : Quel est l'opérateur le plus optimisé dans ce cas?

fromEvent(this.saveButton.nativeElement, 'click')
    .pipe(
        XXX(() => this.fetchProducts(this.form.value))
    )
    .subscribe();

Question 6 : Transformations

. Réponse : exhaustMap

fromEvent(this.saveButton.nativeElement, 'click')
    .pipe(
        exhaustMap(() => this.fetchProducts(this.form.value))
    )
    .subscribe();

Question 6 : Transformations

. Réponse : exhaustMap

Question 7 : Transformations

. Philou doit construire une liste de produits...

 

Question 7 : Transformations

. Petit souci : aucune API n'est disponible côté backend pour récupérer une liste de produits à partir de leurs ids... Seulement une recherche unitaire.

 

Architecte : C'est au front de gérer ça.

Ce ne serait pas plus un problème back...?

Non

OK

Question 7 : Transformations

. Petit souci : aucune API n'est disponible côté backend pour récupérer une liste de produits à partir de leurs ids... Seulement une recherche unitaire.

 

getProducts(ids: number[]): Observable<Product[]> {
  return from(ids).pipe(
    XXX(id => this.httpClient.get<Product>(`product/${id}`))
  );
}

Question 7 : Transformations

. Question : Quel est l'opérateur le plus optimisé dans ce cas?

 

getProducts(ids: number[]): Observable<Product[]> {
  return from(ids).pipe(
    XXX(id => this.httpClient.get<Product>(`product/${id}`))
  );
}

Question 7 : Transformations

. Réponse : mergeMap

 

getProducts(ids: number[]): Observable<Product[]> {
  return from(ids).pipe(
    mergeMap(id => this.httpClient.get<Product>(`product/${id}`))
  );
}

Question 7 : Transformations

. Réponse : mergeMap

 

Question 8

. Bientôt la fin pour Philou, il ne lui reste que la vue profil utilisateur à développer.

Question 8

. Bientôt la fin pour Philou, il ne lui reste que la vue profil utilisateur à développer.

this.userStore$ =
  this.http.get<UserStore>(`api/user-stores/${userId}`);

this.storeName$ = this.userStore$.pipe(
  map(store => store.name)
);
this.storeUrl$ = this.userStore$.pipe(
  map(store => store.url)
);
this.storeCraft$ = this.userStore$.pipe(
  map(store => store.craft)
);
<div class="store-name">{{storeName$ | async}}</div>
<div class="store-url">{{storeUrl$ | async}}</div>
<div class="store-craft">{{storeCraft$ | async}}</div>

Question 8

. Question : Y a t'il un ou des problèmes avec le code ci-dessous?

this.userStore$ =
  this.http.get<UserStore>(`api/user-stores/${userId}`);

this.storeName$ = this.userStore$.pipe(
  map(store => store.name)
);
this.storeUrl$ = this.userStore$.pipe(
  map(store => store.url)
);
this.storeCraft$ = this.userStore$.pipe(
  map(store => store.craft)
);
<div class="store-name">{{storeName$ | async}}</div>
<div class="store-url">{{storeUrl$ | async}}</div>
<div class="store-craft">{{storeCraft$ | async}}</div>

Question 8

. Réponse : Les performances!

this.userStore$ =
  this.http.get<UserStore>(`api/user-stores/${userId}`);

// 1ère requête HTTP...
this.storeName$ = this.userStore$.pipe(
  map(store => store.name)
);
// 2ème requête HTTP...
this.storeUrl$ = this.userStore$.pipe(
  map(store => store.url)
);
// 3ème requête HTTP...
this.storeCraft$ = this.userStore$.pipe(
  map(store => store.craft)
);

Question 9

. Résultat, Philou se fait taper sur les doigts...

Architecte : Il y a trop de charge côté serveur!

Pour une fois que c'est vraiment ma faute...

Quoi?

Non, rien

Question 9

. Il va donc tenter de corriger son code :

. Question : Quelle(s) implémentation(s) sont correctes?

this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    share()
  );
this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    publish(),
    refCount()
  );
this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    multicast(new Subject())
  );
this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    publishReplay(1)
  );

Question 9

. Réponse :

this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    share()
  );
this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    publish(),
    refCount()
  );
this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    multicast(new Subject())
  );
this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    publishReplay(1)
  );

Question 9

. Au début était le multicast...

this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    multicast(new Subject())
  );

this.userStore$.connect();

storeName$

storeUrl$

storeCraft$

Observers :

Subject

userStore$

Source :

Question 9

. Au début était le multicast...

  "Transformation" d'un COLD observable en HOT observable

storeName$

storeUrl$

storeCraft$

Observers :

Subject

userStore$

Source :

Question 9

. Au début était le multicast...

this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    multicast(new Subject()),
    refCount()
  );

Question 9

. Puis le publish...

this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    publish(),
    refCount()
  );

. Et le share.

this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    share()
  );

Question 9

. Et le publishReplay?

this.userStore$ =
  this.http.get(`api/user-stores/${userId}`)
  .pipe(
    multicast(new ReplaySubject(1)),
    refCount()
  );

Subject

BehaviorSubject

ReplaySubject

AsyncSubject

Dernière question...

Question 10 : le meilleur pour la fin

. Philou doit afficher un bouton "show More" pour afficher des détails sur le magasin :

ngOnInit() {
  this.userStore$ =
    this.http.get(`api/user-stores/${userId}`)
    .pipe(
      publish(),
      refCount()
    );
}

showAdditionalData = false;

function showMore() {
  this.showAdditionalData = true;
  this.storeComments$ = this.userStore$.pipe(
    map(store => store.comments)
  );
  this.storePictures$ = this.userStore$.pipe(
    map(store => store.pictures)
  );
}
<button (click)="showMore()">
  Show more
</button>
<div *ngIf="showAdditionalData">
  <div class="store-comments">
    {{storeComments$ | async}}
  </div>
  <div class="store-pictures">
    {{storePictures$ | async}}
  </div>
</div>

. Question : Que va t'il se passer au clic sur le bouton?

ngOnInit() {
  this.userStore$ =
    this.http.get(`api/user-stores/${userId}`)
    .pipe(
      publish(),
      refCount()
    );
}

showAdditionalData = false;

function showMore() {
  this.showAdditionalData = true;
  this.storeComments$ = this.userStore$.pipe(
    map(store => store.comments)
  );
  this.storePictures$ = this.userStore$.pipe(
    map(store => store.pictures)
  );
}
<button (click)="showMore()">
  Show more
</button>
<div *ngIf="showAdditionalData">
  <div class="store-comments">
    {{storeComments$ | async}}
  </div>
  <div class="store-pictures">
    {{storePictures$ | async}}
  </div>
</div>

Question 10 : le meilleur pour la fin

. Réponse : RIEN

. Pourquoi?

Subject

userStore$

Question 10 : le meilleur pour la fin

1. Des observers souscrivent au subject.

2. Le Subject souscrit à la source.

Subject

userStore$

storeName$

storeUrl$

storeCraft$

Question 10 : le meilleur pour la fin

3. La source émet au Subject...

4. ... Qui émet aux observers

Subject

userStore$

storeName$

storeUrl$

storeCraft$

Question 10 : le meilleur pour la fin

4. La source est completed.

5. Le subject est completed.

Subject

userStore$

Question 10 : le meilleur pour la fin

6. De nouveaux observers s'abonnent...

7. ... Mais ne reçoivent qu'un "completed".

Subject

userStore$

storeComments$

storePictures$

Question 10 : le meilleur pour la fin

. Et avec un share?

ngOnInit() {
  this.userStore$ =
    this.http.get(`api/user-stores/${userId}`)
    .pipe(
      share()
    );
}

showAdditionalData = false;

function showMore() {
  this.showAdditionalData = true;
  this.storeComments$ = this.userStore$.pipe(
    map(store => store.comments)
  );
  this.storePictures$ = this.userStore$.pipe(
    map(store => store.pictures)
  );
}

. L'API distante va être rappellée...

Question 10 : le meilleur pour la fin

. Solution :

ngOnInit() {
  this.userStore$ =
    this.http.get(`api/user-stores/${userId}`)
    .pipe(
      publishReplay(1),
      refCount()
    );
}
ngOnInit() {
  this.userStore$ =
    this.http.get(`api/user-stores/${userId}`)
    .pipe(
      shareReplay(1)
    );
}

Question 10 : le meilleur pour la fin

Et le vainqueur est?

En 3ème position...

. Une place pour le devOps Day Marseille

. Une place RdvConnect

En 2ème position...

. Une place pour le devFest Toulouse

. Une place RdvConnect

En 1ère position...

. IPAD AIR 64Go

En conclusion

. RxJS est une librairie...

  . Qui fait des appels http avec Angular.

  . Très (très) puissante.

  . Qui factorise la maintenabilité / relecture.

  . Qui pousse à la programmation fonctionnelle.

 

Merci à tous!

Advanced RxJS concepts

By Laurent WROBLEWSKI

Advanced RxJS concepts

  • 462