RxJS è una libreria JavaScript basata sul pattern Observable, che permette di gestire eventi asincroni in maniera funzionale e reattiva. Il pattern Observable (anche noto come pattern Publisher-Subscriber o pattern Event-Dispatcher) è un design pattern comportamentale che consente di definire una dipendenza uno-a-molti tra oggetti in modo che quando un oggetto cambia stato, tutti gli oggetti dipendenti da esso vengono notificati e aggiornati automaticamente.
In questo pattern, l'oggetto che cambia stato è chiamato "Observable" o "Subject", mentre gli oggetti che dipendono da esso sono chiamati "Observer" o "Subscriber". L'Observable mantiene una lista di tutti gli Observer registrati e quando cambia stato, notifica tutti gli Observer registrati chiamando un metodo aggiornamento (update) su ciascuno di essi.
L'uso del pattern Observable consente di creare un sistema altamente modulare e scalabile in cui gli oggetti possono essere facilmente aggiunti o rimossi senza influire sul funzionamento degli altri oggetti. Inoltre, il pattern Observable è particolarmente utile in situazioni in cui si desidera separare la logica di visualizzazione dalla logica di business, in quanto consente di aggiornare la vista automaticamente ogni volta che cambia lo stato dell'oggetto osservato.
I flussi di dati possono essere manipolati e combinati tra loro, fornendo una soluzione elegante e potente per la gestione di eventi asincroni in JavaScript. In altre parole, RxJS fornisce uno strumento potente per gestire determinati flussi, come ad esempio le chiamate HTTP, gli eventi del DOM o i dati provenienti da una socket.
RxJS è uno dei concetti più complessi di Angular, quindi proverò a spiegarlo con una metafora. In un video ho visto che veniva paragonato ad un fast food… e inoltre, si spiegava che il cliente porta tutti gli ingredienti che vengono lavorati da quattro cuochi, ognuno con un singolo compito. L'unico ingrediente fornito dal fast food è il pane...Alla fine il cliente usciva dal fast food con il miglior hamgurger del mondo.
Come possiamo spiegare questa metafora? Secondo la mia interpretazione, gli ingredienti sono i dati che arrivano dall'Observable, il pane è fornito dal fast food in quanto prodotto standard e i cuochi sono gli operatori della pipe che gestiscono il flusso e possono modificare il risultato finale.
In sintesi, la metafora del fast food rappresenta l'idea che RxJS fornisce gli strumenti per manipolare i flussi di dati asincroni in modo flessibile e modulare, consentendo al programmatore di applicare le proprie operazioni per ottenere il risultato desiderato.
Tuttavia, c'è un’altra analogia importante: anche se un fast food offre un'esperienza immediata e soddisfacente, può essere poco salutare e avere effetti negativi sulla salute a lungo termine. Allo stesso modo, se non viene utilizzata correttamente, RxJS può portare a problemi di comprensione del codice e di gestione degli errori.
Per questo motivo, è importante utilizzare RxJS con attenzione e utilizzare le pratiche consigliate per garantire che il codice sia mantenibile e scalabile nel tempo.
Observable e Observer
Un Observable in RxJS è un oggetto che rappresenta un flusso di dati asincroni, che possono essere eventi, valori numerici, oggetti, stringhe, o qualsiasi altro tipo di dato. Il flusso può essere generato in molti modi diversi, ad esempio da eventi DOM, richieste HTTP, timeout, letture di file, sensori, o altri flussi di dati generati dal codice.
Gli Observable sono formati da tre parti principali:
- Producer: è la parte del codice che produce e invia i dati nel flusso Observable. Può essere un evento, una richiesta HTTP, o qualsiasi altra fonte di dati asincroni.
- Subscription: è il meccanismo che consente ad un Observer di osservare (o "sottoscriversi") all'Observable, per ricevere i dati prodotti dal Producer.
- Observer: è un oggetto o una funzione che gestisce i dati emessi dall'Observable. In particolare, l'Observer definisce tre metodi principali: next(), error() e complete(). Quando l'Observable emette un valore, viene chiamato il metodo next() dell'Observer. Se l'Observable genera un errore, viene chiamato il metodo error(). Infine, quando l'Observable termina il flusso, viene chiamato il metodo complete().
Una volta che un Observer si sottoscrive all'Observable, inizia ad ascoltare i dati emessi e a reagire di conseguenza. Ad esempio, un Observer può aggiornare l'interfaccia utente in base ai dati ricevuti, o elaborare i dati per produrre nuovi risultati.
In RxJS, gli Observable possono essere combinati, trasformati e filtrati per creare flussi di dati più complessi. Ci sono molte operazioni disponibili per manipolare gli Observable, come map(), filter(), merge(), concat(), debounceTime(), scan(), e molte altre.
Come utilizzare gli Observable in Angular
Come abbiamo anticipato, gli Observable sono utilizzati in Angular per gestire eventi asincroni. Ad esempio, quando si fa una richiesta HTTP, utilizzando il modulo predefinito di Angular, per ottenere i dati dal server, il server risponde con un Observable da utilizzare per gestire la risposta e aggiornare la vista dell'applicazione. Vedremo in seguito come usare l'HttpClient
di Angular, per adesso limitiamoci ad usare un Observable "fatto in casa".
Esempio di utilizzo di Observable in un servizio
Vediamo come usare gli Observables nel servizio che abbiamo creato nella lezione precedente. Per trasformare il ritorno del metodo getData()
in un Observable, possiamo utilizzare l'operatore of()
di RxJS.
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MyServiceService {
private products = [
{ id: 1, name: 'Prodotto 1', price: 9.99 },
{ id: 2, name: 'Prodotto 2', price: 19.99 },
{ id: 3, name: 'Prodotto 3', price: 29.99 }
];
constructor() { }
getData(): Observable<any> {
return of(this.products);
}
}
Notiamo che il tipo restituito dal metodo getData()
è stato modificato in Observable
per indicare che il metodo restituisce un Observable che emette qualsiasi tipo di dati.
In questo modo, possiamo utilizzare il metodo getData() come un Observable in Angular e sottoscriverci ad esso in modo da ricevere l'array dei prodotti quando viene emesso. E come possiamo sottoscriverci al nostro nuovo Observable? Dobbiamo modificare anche il metodo del componente che utilizza il nostro servizio, quindi riprendiamo il codice del componente che abbiamo usato nella lezione precedente è modifichiamolo in modo da "osservare l'osservabile".
import { Component, OnInit } from '@angular/core';
import { MyService } from './my.service';
@Component({
selector: 'app-my-component',
template: '<h1>{{ data }}</h1>'
})
export class MyComponent implements OnInit {
products: ({ price: number; name: string; id: number })[] = [];
constructor(private myService: MyService) {
}
ngOnInit(): void {
this.test.getData().subscribe((data: any) => this.products = data);
console.log(this.products)
}
}
La modifica risiede nell'uso del metodo subscribe()
sull'Observable restituito dal servizio. Il metodo subscribe()
viene usato per reagire alle emissioni dati dell'Observable.
Quando viene emesso un dato, il callback passato al metodo subscribe() viene richiamato, e l'array products del componente viene assegnato ai dati emessi. Nell'esempio, il callback assegna i dati emessi alla proprietà products del componente.
Il metodo subscribe()
dell'oggetto Observable
definisce tre diverse callback, che possono essere definite come argomenti della funzione subscribe()
stessa:
-
La callback per le emissioni di dati (
next()
): questa callback viene chiamata ogni volta che l'Observable
emette un nuovo valore. L'argomento della callback è il valore emesso. -
La callback per gli errori (
error()
): questa callback viene chiamata se si verifica un errore durante l'emissione dei dati. L'argomento della callback è l'errore che si è verificato. -
La callback per la completamento (
complete()
): questa callback viene chiamata quando l'Observable
ha completato la sua emissione di dati. Non viene passato nessun argomento alla callback.
Ad esempio, la seguente chiamata al metodo subscribe()
definisce tutte e tre le callback:
myObservable.subscribe(
(value) => console.log('Valore emesso:', value),
(error) => console.error('Errore:', error),
() => console.log('Completato')
);
In questo esempio, la prima callback stampa il valore emesso, la seconda callback stampa l'errore verificatosi e la terza callback stampa "Completato" quando l'Observable
termina la sua emissione di dati.