Elementi fondamentali di NgRx: Store, Azioni, Riduttori, Selettori, Effetti

Lo store è l’elemento chiave dell’intero processo di gestione dello stato. Tiene lo stato e facilita l’interazione tra i componenti e lo stato. È possibile ottenere un riferimento al negozio tramite l’iniezione di dipendenza di Angular, come mostrato di seguito.

constructor(private store: Store<AppState>) {}

Questo riferimento allo store può essere successivamente utilizzato per due operazioni principali:

  • Per inviare azioni al negozio tramite il metodo store.dispatch(…), che a sua volta attiverà riduttori ed effetti
  • Per recuperare lo stato dell’applicazione tramite i selettori

Struttura di un albero di oggetti di stato

Supponiamo che la vostra applicazione consista di due moduli funzionali chiamati Utente e Prodotto. Ognuno di questi moduli gestisce parti diverse dello stato complessivo. Le informazioni sul prodotto saranno sempre mantenute nella sezione products dello stato. Le informazioni sull’utente saranno sempre mantenute nella sezione user dello stato. Queste sezioni sono anche chiamate slices.

Azioni

Un’azione è un’istruzione che viene inviata al negozio, opzionalmente con alcuni metadati (payload). In base al tipo di azione, il negozio decide quali operazioni eseguire. Nel codice, un’azione è rappresentata da un semplice oggetto JavaScript con due attributi principali, cioè type e payload. payload è un attributo opzionale che sarà usato dai riduttori per modificare lo stato. Il seguente frammento di codice e la figura illustrano questo concetto.

{ "type": "Login Action", "payload": { userProfile: user }}

NgRx versione 8 fornisce una funzione di utilità chiamata createAction per definire creatori di azioni (non azioni, ma creatori di azioni). Segue un esempio di codice per questo.

Si può quindi utilizzare il creatore di azioni login (che è una funzione) per costruire azioni e spedirle al negozio come mostrato di seguito. user è l’oggetto payload che si passa nell’azione.

this.store.dispatch(login({user}));

Riduttori

I riduttori sono responsabili della modifica dello stato e della restituzione di un nuovo oggetto stato con le modifiche. I riduttori prendono due parametri, lo stato corrente e l’azione. In base al tipo di azione ricevuta, i riduttori eseguiranno alcune modifiche allo stato corrente e produrranno un nuovo stato. Questo concetto è mostrato nel diagramma qui sotto.

Similmente alle azioni, NgRx fornisce una funzione di utilità chiamata createReducer per creare riduttori. Una tipica chiamata di funzione createReducer sarebbe la seguente.

Come potete vedere, prende lo stato iniziale (lo stato all’avvio dell’applicazione) e funzioni di cambio di stato uno a molti che definiscono come reagire a diverse azioni. Ognuna di queste funzioni di cambio di stato riceve lo stato corrente e l’azione come parametri, e restituisce un nuovo stato.

Effetti

Gli effetti permettono di eseguire effetti collaterali quando un’azione viene spedita al negozio. Cerchiamo di capire questo attraverso un esempio. Quando un utente accede con successo a un’applicazione, un’azione con type Login Action sarà inviata al negozio con le informazioni dell’utente nel payload. Una funzione riduttrice ascolterà questa azione e modificherà lo stato con le informazioni dell’utente. Inoltre, come effetto collaterale, si vuole anche salvare le informazioni dell’utente nella memoria locale del browser. Un effetto può essere usato per eseguire questo compito aggiuntivo (effetto collaterale).

Ci sono diversi modi per creare effetti in NgRx. Quello che segue è un modo grezzo e autoesplicativo di creare effetti. Si prega di notare che generalmente non si usa questo metodo per creare effetti. L’ho preso solo come esempio per spiegare cosa succede dietro la tenda.

  • actions$ osservabile emetterà azioni ricevute dal negozio. Questi valori passeranno attraverso una catena di operatori.
  • ofType è il primo operatore utilizzato. Questo è un operatore speciale fornito da NgRx (Not RxJS) per filtrare le azioni in base al loro tipo. In questo caso, solo le azioni di tipo login saranno autorizzate a passare attraverso il resto della catena di operatori.
  • tap è il secondo operatore utilizzato nella catena per memorizzare le informazioni dell’utente nella memoria locale del browser. L’operatore tap è generalmente usato per eseguire effetti collaterali in una catena di operatori.
  • Infine, dobbiamo sottoscrivere manualmente l’osservabile login$.

Tuttavia, questo approccio ha un paio di svantaggi importanti.

  • Devi sottoscrivere manualmente l’osservabile, che non è una buona pratica. In questo modo, dovrete sempre disiscrivervi manualmente, il che porta alla mancanza di manutenibilità.
  • Se si verifica un errore nella catena degli operatori, l’osservabile andrà in errore e smetterà di emettere valori successivi (azioni). Di conseguenza, l’effetto collaterale non sarà eseguito. Pertanto, dovete avere un meccanismo in atto per creare manualmente una nuova istanza di osservabile e risottoscrivere se si verifica un errore.

Per superare questi problemi, NgRx fornisce una funzione di utilità chiamata createEffect per creare effetti. Una tipica chiamata alla funzione createEffect sarebbe come la seguente.

Il metodo createEffect prende una funzione che restituisce un osservabile e (opzionalmente) un oggetto di configurazione come parametri.

NgRx gestisce la sottoscrizione all’osservabile restituito dalla funzione di supporto, e quindi non è necessario iscriversi o cancellarsi manualmente. Inoltre, se si verifica un errore nella catena degli operatori, NgRx crea un nuovo osservabile e si risottoscrive per garantire che l’effetto collaterale venga sempre eseguito.

Se dispatch è true (valore predefinito) nell’oggetto di configurazione, il metodo createEffect restituisce un Observable<Action>. Altrimenti, restituisce un Observable<Unknown>. Se la proprietà dispatch è true, NgRx si iscriverà all’osservabile restituito di type Observable<Action> e spedirà le azioni ricevute al negozio.

Se non state mappando l’azione ricevuta a un diverso tipo di azione nella catena degli operatori, dovrete impostare dispatch su false. Altrimenti, l’esecuzione risulterà in un ciclo infinito, poiché la stessa azione sarà spedita e ricevuta nel flusso actions$ ancora e ancora. Per esempio, non è necessario impostare dispatch a false nel codice qui sotto perché si mappa l’azione originale a un diverso tipo di azione nella catena dell’operatore.

Nello scenario precedente,

  • Effetto riceve azioni di type loadAllCourses.
  • Viene invocata un’API e i corsi vengono caricati come effetto collaterale.
  • La risposta dell’API a un’azione di type allCoursesLoaded viene mappata e i corsi caricati vengono passati come payload all’azione.
  • E infine, l’azioneallCoursesLoaded che è stata creata viene spedita al negozio. Questo viene fatto da NgRx sotto il cofano.
  • Un riduttore ascolterà l’azione allCoursesLoaded in arrivo e modificherà lo stato con i corsi caricati.

Selettori

I selettori sono funzioni pure usate per ottenere fette dello stato del negozio. Come mostrato di seguito, è possibile interrogare lo stato anche senza usare i selettori. Ma questo approccio ha di nuovo un paio di contro importanti.

const isLoggedIn$ = this.store.pipe(map(state => !!state.user));
  • store è un osservabile a cui si può sottoscrivere. Ogni volta che il negozio riceve un’azione, store spingerà l’oggetto di stato ai suoi sottoscrittori.
  • Si possono usare funzioni di mappatura per ottenere fette di stato ed eseguire qualsiasi calcolo, se necessario. Nell’esempio precedente, stiamo ottenendo la fetta user nell’albero degli oggetti di stato e la convertiamo in un booleano per determinare se l’utente ha effettuato l’accesso o meno.
  • Si può sottoscrivere manualmente l’osservabile isLoggedIn$ o utilizzarlo in un template Angular con pipe async per leggere i valori emessi.

Tuttavia, questo approccio ha un grosso svantaggio. In generale, il negozio riceve frequentemente azioni da diverse parti dell’applicazione. Secondo l’implementazione di cui sopra, ogni volta che il negozio riceve un’azione, un oggetto di stato sarà emesso dal negozio. E questo oggetto di stato passerà nuovamente attraverso la funzione di mappatura e aggiornerà l’UI.

Tuttavia, se il risultato della funzione di mappatura non è cambiato dall’ultima volta, non c’è bisogno di aggiornare nuovamente l’UI. Per esempio, se il risultato della map(state => !!state.user) non è cambiato dall’ultima esecuzione, non dobbiamo di nuovo spingere il risultato sull’UI/Subscriber. Per ottenere questo, NgRx (non RxJS) ha introdotto un operatore speciale chiamato select. Con l’operatore select, il codice di cui sopra cambierà come segue.

const isLoggedIn$ = this.store.pipe(select(state => !!state.user));

L’operatore select impedirà che i valori vengano spinti all’UI/subscriber se il risultato della funzione di mappatura non è cambiato dall’ultima volta.

Questo approccio può essere ulteriormente migliorato. Anche se l’operatore select non spinge valori invariati all’UI/Subscribers, deve comunque prendere l’oggetto di stato e fare il calcolo per ricavare il risultato ogni volta.

Come già spiegato sopra, un state sarà emesso dall’osservabile quando il negozio riceve un’azione dall’applicazione. Un’azione non sempre aggiorna lo stato. Se lo stato non è cambiato, anche il risultato del calcolo della funzione di mappatura non cambierà. Quindi non dobbiamo rifare il calcolo se l’oggetto state emesso non è cambiato dall’ultima volta. È qui che entrano in gioco i selettori.

Un selettore è una funzione pura che mantiene una memoria delle esecuzioni precedenti. Finché l’input non è cambiato, l’output non sarà ricalcolato. Invece, l’output sarà restituito dalla memoria. Questo processo è chiamato memoization.

NgRx fornisce una funzione di utilità chiamata createSelector per costruire selettori con capacità di memoization. Segue un esempio della funzione di utilità createSelector.

La funzione createSelector accetta funzioni di mappatura uno-a-molti che danno diverse fette dello stato e una funzione proiettore che esegue il calcolo. La funzione proiettore non sarà invocata se le fette di stato non sono cambiate dall’ultima esecuzione. Per utilizzare la funzione selettore creata, dovete passarla come argomento all’operatore select.

this.isLoggedIn$ = this.store .pipe( select(isLoggedIn) );

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.