Introduzione RXJS – Parte 5: Operatori e conclusione

Home/code, italian, javascript/Introduzione RXJS – Parte 5: Operatori e conclusione

Introduzione RXJS – Parte 5: Operatori e conclusione

RxJS esprime il massimo potenziale quando utilizzato con i suoi operatori, semplici funzioni che si dividono essenzialmente in due macro categorie:

  • Operatori di creazione: funzioni che possono essere utilizzate per creare Observable
  • Pipeaple Operators: che in realtà si potrebbero dividere in altre sottocategorie (condizionali, trasformazione, filtri, multicast, errore, ecc.)

Operatori di Creazione

Come già anticipato, l’obiettivo di questi operatori è quello di generare degli Observable, a partire da qualunque tipo di scenario: forms, XHR, timers, eventi e così via.

Operatore interval

Un esempio tipico di operatore di creazione è interval, che emette un valore incrementale, nel tempo, partendo da zero:

import { interval } from 'rxjs';

const source = interval(1000);

source.subscribe(
  x => console.log(x)
)

// output:
// 0
// 1
// 2
// ...
// N

Operatore of

L’operatore of emette invece un determinato numero di valori in sequenza e poi “completa” l’observable:

import { of } from 'rxjs';

const source = of(10, 20, 30);

source.subscribe(
  x => console.log(x),
  err => console.log(err),
  () => console.log('completed')
)

// output:
// 10
// 20
// 30
// completed

Operatore fromEvent

L’operatore di creazione fromEvent converte, invece, un evento di qualsiasi tipo in un Observable, spesso utilizzato con MouseEvent e KeyboardEvent.

Per dare un po’ di senso a questi esempi, introdurremo ora alcuni operatori “pipeable” al fine di trarre il massimo beneficio dall’utilizzo degli Observable e comprendere i benefici di un approccio funzionale rispetto ad uno imperativo.
Degli operatori “pipeable” ne parleremo più in dettaglio nella seconda parte dell’articolo.

Nel prossimo snippet utilizziamo, quindi, anche l’operatore debounceTime che ci permetterà di ignorare i valori emessi in un lasso di tempo inferiore a quello specificato.

Questo operatore è utilizzato di frequente con i form, quando, ad esempio, non abbiamo la necessità di invocare una determinata operazione, in questo caso la chiamata ad una API REST, ad ogni evento ‘input’ di tastiera ma solo dopo un determinato lasso di tempo dall’ultima digitazione, ovvero quando l’utente ha terminato di digitare tutto il testo.
In questo modo eviteremo di invocare l’API ad ogni pressione di un tasto della tastiera, ma solo al termine.

Immaginiamo di avere un template HTML che contenga un semplice campo di input:

<input id="myInput">

Invece di utilizzare il classico metodo addEventListener per registrare un evento, utilizzeremo l’operatore di creazione fromEvent per effettuare la stessa operazione (ottenendo però un Observable) e potremo quindi sfruttare gli operatori “pipeable” di RxJS per gestire il flusso dati, in questo caso applicando semplicemente un effetto “debounce”:

import { fromEvent } from 'rxjs'; 
import { debounceTime } from 'rxjs/operators';

fromEvent(document.getElementById('myInput'), 'input')
  .pipe(
    debounceTime(1000),
  )
  .subscribe((event: KeyboardEvent) => {
    console.log('call your REST API:'+ event.target.value)
  })

Ma possiamo fare anche di più, come nell’esempio seguente:

  • dato che a noi interessa solo il valore del campo di input e non l’evento di per sè, possiamo far in modo che la funzione next dell’observer riceva direttamente il value e non l’evento, trasformando quindi event.target.value in event(tramite l’utilizzo dell’operatore map)
  • applicare filtri per evitare sia emesso un valore se la parola digitata non contiene almeno 3 caratteri (operatore filter),
  • evitare che sia emesso un valore se identico al precedente (distinctUntilChanged)
  • e così via…
import { fromEvent } from 'rxjs'; 
import { debounceTime } from 'rxjs/operators';

fromEvent(document.getElementById('myInput'), 'input')
  .pipe(
    map(event => event.target.value ),
    filter(text => text.length >= 3 ),
    debounceTime(1000),
    distinctUntilChanged()
  )
  .subscribe(text: string) => {
    console.log('call your REST API:'+ text)
  })

Senza un approccio funzionale (o RxJS) avremmo dovuto inserire delle condizioni (if) per sostituire le operazioni di filter e distinctUntilChanged, usare un timer per il debounce e probabilmente creare qualche variabile locale al posto del map.

Ora provate ad immaginare quante righe di codice potreste risparmiare in un’applicazione real-world e quanto il vostro codice potrebbe diventare conciso ed espressivo con questo approccio. Penso non serva aggiungere altro.

NOTA per gli sviluppatori Angular: lo stesso approccio può essere utilizzato con i Reactive Forms, tramite i quali possiamo utilizzare il paradigma reattivo non solo per gestire singoli campi di input ma anche interi form, porzioni di form (FormGroup) e così via. Lo stesso approccio potrà comunque essere utilizzato anche per gestire eventi del router, HTTP interceptor, guardie del router, event emitter dei componenti e molto altro, dato che il framework è totalmente basato su RxJS, dipendenza fondamentale del framework.

Pipeable Operators

Questi operatori sono funzioni pure che prendono un Observable come input e restituiscono un altro Observable come output.

Come abbiamo già visto in alcuni esempi dove abbiamo utilizzato filter, map, scan, take, debounceTime, ecc. abbiamo a disposizione decine di operatori pipeable a nostra disposizione, che possono essere usati singolarmente oppure concatenati con la funzione pipe():

obs.pipe(
  op1(),
  op2(),
  op3(),
  op3(),
)

Per l’elenco completo degli operatori disponibili potete consultare la reference di RxJS

Operatori filter, map, take

Con un approccio molto simile a quello utilizzato per la gestione dell’input del paragrafo precedente, utilizziamo ora l’operatore di creazione interval e diversi operatori per eseguire diverse operazioni:

  • filter: non emette i valori 0 e 1 ma comincia da 2
  • map: trasforma il valore emesso dall’observable moltiplicandolo per 10
  • take: prende solo i primi 5 valori emessi e poi l’esecuzione viene completata
import { interval } from 'rxjs';
import { map, filter, take } from 'rxjs/operators';

const observable = interval(1000)
.pipe(
  filter(x => x > 2), 
  map(x => x * 10),
  take(5),
)
observable.subscribe(
  x => console.log(x),  
  err => console.log(err),
  () => console.log('complete')
)

// Output (i primi due valori, 10 e 20 non vengono emessi)
// 30
// 40
// 50
// 60
// 70
// complete

Imperative vs Reactive programming

Grazie all’utilizzo di funzioni pure, lo stato che si sta manipolando è isolato, rendendo il codice meno soggetto ad errori e a fattori esterni, dato che il codice strutturato con questo paradigma non prevede l’influenza da effetti esterni (“side effects”) come cambi di route, salvataggi su localstorage, utilizzo di servizi e/o variabili esterne.

Un esempio di funzione “impura”:

let count = 1;

document.addEventListener('click', () => {
  if (count > 2) {
    console.log(Math.random())
  }
  count++;
});

Gli operatori sono invece un validissimo strumento per controllare il flusso di eventi, manipolare i valori emessi dagli observables o passare, ad esempio, da un tipo di observable ad un altro.

Nel prossimo esempio sfruttiamo gli operatori scan e filter, molto simili rispettivamente alle funzioni reduce e filter disponibili per gli array in Javascript, per ottenere lo stesso risultato del precedente script utilizzando un approccio idiomatico con RxJS:

  • scan: accumula valori, partendo da 0 e incrementando il valore di 1 ad ogni click
  • filter: emette un valore solo quando l’espressione fornita restituisce true
  • map: applica una funzione di proiezione ad ogni valore emesso dall’observable
import { fromEvent } from 'rxjs'; 
import { scan, map, filter } from 'rxjs/operators';

fromEvent(document, 'click')
  .pipe(
    scan(acc => acc + 1, 0),
    filter(count => count >2),
    map(event => Math.random())
  )
  .subscribe(res => {
    console.log(res);
  })

AJAX

Per proporvi un esempio “meno teorico” utilizzeremo l’operatore di creazione ajax che ci permetterà di invocare delle API REST.

In questo esempio incrementiamo un counter da 1 a N ad ogni click dell’utente ed effettuiamo una chiamata REST passando tale counter come parametro della request. Gli operatori coinvolti in questo esempio sono:

  • scan: incrementa un counter di 1, partendo da 0
  • takeWhile: emette un valore solo quando l’espressione fornita restituisce true e COMPLETA l’observable quando è false
  • switchMap: lo scopo di questo operatore è quello di “passare” da un observable (outer, in questo caso generato da fromEvent) ad un nuovo inner observable, ovvero la chiamata AJAX, completare eventuali precedenti inner observable, se in pending, ed emettere dei valori dal nuovo (inner) observable. Per il momento, questa definizione potrebbe sembrarvi davvero complicata da comprendere ma possiamo semplificarla in “Passare (switch) da un Observable ad un altro”
  • ajax: crea un observable per effettuare una richiesta XHR
import { fromEvent } from 'rxjs'; 
import { ajax } from 'rxjs/ajax'; 
import { scan, map, switchMap, takeWhile } from 'rxjs/operators';


fromEvent(document, 'click')
  .pipe(
    scan(acc => acc + 1, 0),
    takeWhile(value => value <= 3),
    switchMap(value => ajax(`https://jsonplaceholder.typicode.com/users/${value}`)),
    map(res => res.response),
  )
  .subscribe(res => {
    console.log(res);
  })

switchMap, mergeMap, concatMap e exhaustMap sono tra gli operatori RxJS più utilizzati ma richiedono un po’ di studio e tempo per essere digeriti. Le differenze tra questi 4 operatori sono davvero minime ma fondamentali da comprendere per gestire nel modo corretto, ad esempio, sequenze di operazioni CRUD asincrone. La scelta di un operatore errato potrebbe compromettere totalmente il funzionamento dell’applicazione.
Il mio consiglio è semplicemente quello di leggervi più articoli, guide e tutorial su RxJS e su questi (e altri) operatori e semplicemente sperimentare molto.

Conclusione

È ovvio che questa serie di articoli non rappresenta una guida esaustiva all’utilizzo di RxJS ma ha il solo obiettivo di avvicinarvi alla programmazione funzionale reattiva.

Le motivazioni per le quali ritengo RxJS una libreria fantastica (quasi indispensabile oggi giorno non solo nel mondo front-end ma anche back-end) le ho descritte nel primo articolo di questa serie, tuttavia, vorrei evidenziare alcuni aspetti che ritengo interessanti riguardo questo approccio, prendendo spunto da alcune slide sulla programmazione funzionale pubblicate da Mattia Natali.

Innanzitutto mi è piaciuto il titolo delle slide perchè rende subito l’idea:

Write Code for Humans! Not for Machines

Altri due concetti fondamentali sono rappresentati benissimo in queste due slides:

E concludo traducendo alcune porzioni di slide che riassumono benissimo dei concetti che condivido:

Perché gli essere umani preferiscono la programmazione funzionale?

  • Si focalizza su COSA vuoi ottenere come risultato
  • Riduce la quantità di codice
  • Nasconde i dettagli dell’implementazione

Perché le macchine preferiscono la programmazione imperativa?

  • Si focalizza su COME vuoi ottenere il risultato
  • Si scrive il codice necessario a far compiere alla macchina delle operazioni
  • Performance (non sempre)

Quando scrivi codice devi ricordare che:

  • Qualcuno revisionerà il tuo codice
  • I vostri attuali e futuri colleghi devono essere in grado di comprendere il tuo codice
  • Qualcuno potrebbe ispirarsi al tuo codice
  • … o creare qualcosa che si basa su di esso

Hardware is Cheap, Developers are Expensive

Spero che questa breve serie di articoli ti sia piaciuta.

Per rimanere aggiornato sui materiali che pubblico, sui corsi e sugli eventi che organizzo seguimi sulla mia pagina Facebook o LinkedIn.

2019-11-17T01:59:40+00:00 novembre 14th, 2019|code, italian, javascript|

Leave A Comment