RxJS
Quizz : Concepts et cas d'usage avancés


Laurent Wroblewski
Philippe Beroucry


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 boursierQuestion 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 :

combineLatestzip
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