Gestione dello stato dell’applicazione con il pattern redux/ngrx in Angular

Gli albori con Angular

Ho iniziato a utilizzare Angular nel 2016, quando ancora era in Beta. Si trovava pochissima documentazione e best-practice, a parte i materiali forniti da John Papa (indiscusso riferimento su Angular e AngularJs). Quindi, come molti sviluppatori all’epoca, per mantenere lo stato dell’applicazione ho utilizzato un Service, appositamente progettato per essere l’unico entry-point della gestione dello stato. Oltre a questo, per la comunicazione con il Backend, un utilizzo esclusivo di Promises e Deferred Promises. Gli Observables non erano ancora molto diffusi, se non per il binding con i controlli direttamente sulle Form.

Gli Observables

Ho introdotto gli Observables quando mi è nata la necessità di avere una Promise “cancellabile”, ovvero avevo la necessità di una comunicazione backend per ogni tasto premuto (una sorta di campo di ricerca) e volevo ignorare le richieste precedenti, in quanto non valide, in quanto ero incorso nel fastidioso bug delle risposte out-of-order: ovvero ricevevo una risposta dal backend dopo averne ricevuta una più aggiornata. Impossibile risolvere questo problema in modo pulito con una Promise, da qui lo studio ed introduzione degli Observables e rxjs.

Dove sono i pattern su Angular?

Ultimamente discutevo con un mio collega – fullstack come me, ma più esperto di backend – della mancanza di un pattern chiaro su Angular, simile a Command Query Responsibility Segragation (CQRS) e Event Sourcing (ES) che trovo molto intelligente e una ottima idea per mantenere un progetto nel lungo termine. Dopo una ricerca abbiamo trovato il pattern Redux, che praticamente è CQRS+ES per il frontend! Dopo aver valutato alcune librerie, ho optato per ngrx (ngrx/core, ngrx/store, ngrx/effetcs).

Cos’è Redux?

Redux è un pattern non legato a nessuna piattaforma specifica, anche se sviluppato nell’ambito React. Redux è un pattern per la gestione dello stato di una applicazione. Ci sono quindi implementazioni specifiche di Redux per Angular, tipo ngrx. Ngrx estende (supercharge) RxJS e richiede un buon livello di comprensione di RxJS. Qui potete trovare una ottima spiegazione e reference su RxJS e i metodi implementati (map, switchMap, reduce, ecc).

ngrx supercharge rxjs

Come funziona il pattern Redux?

Il concetto principale è la creazione di un unico Store contenente lo stato dell’applicazione, modificato tramite dei Reducers invocati da Actions, con l’appoggio di Effects se una Action necessita di una comunicazione esterna (Side effect, come ad esempio una chiamata al backend).

ngrx redux pattern diagram

In pratica il flusso di “lettura” di una “variabile” (o parte dello stato) – come per esempio un array di entità – si tratta solamente di avere un observable nel nostro Component che punta a una parte dello stato. Ad esempio:

companies$ = this.store.select(“companies”);

Si noti l’utilizzo del simbolo $ per indicare che quella variabile è un Observable (best-practice utilizzata non solo con Redux, ma con ogni Observable, per chiarezza sappiamo è un Observable e non un tipo “normale”).

mentre per modificare lo stato basta invocare una azione dal nostro Component. Ad esempio:

this.store.dispatch(new CompanyDeleteAction(companyId));

A questo punto il nostro Observable “companies$” verrà aggiornato (e la UI di conseguenza), in quanto lo Store notifica il cambiamento dello stato agli Observable “in ascolto”.

La parte più complessa è nell’implementazione dell’azione. L’azione ha un nome (una stringa) e un “payload”. In base al nome dell’azione, uno specifico Reducers viene invocato.

Store e Reducers

Cos’è un Reducer? Il nome Reducer nasce dall’utilizzo dell’operatore “Reduce” degli Observable. Un Reducer è una pure-function con due parametri e un valore di ritorno. Cos’è una pure-function? Una pure-function ritorna sempre lo stesso risultato a parità di input, e non modifica lo stato di nessuna variabile esterna – ad esempio una variabile globale. Quindi riassumendo:

  • [input] 1° parametro: lo stato precedente,
  • [input] 2° parametro: un payload,
  • [output] il Reducer si deve limitare a tornare lo stato aggiornato.

Ora lo State è stato aggiornato e gli Observable in ascolto notificati.

Vediamo un esempio con un flusso che non interagisce con il backend, come il cambio di stato di un elemento, tipo apri-chiudi di un menu:

User click -> Action “ShowMenuAction(true)” -> Reducer “ShowMenuReducer(store, payload)” -> Store “showOpen = true” -> Observable “showMenu$” aggiornato

Backend e Effects

Ma se vogliamo avere una chiamata al backend quindi come possiamo fare? Non è possibile farla nel Reducer, in quanto violeremmo il principio della pure-function. Qui entrano in gioco gli Effects (o Side Effects). In pratica l’Effect è in ascolto di ogni reducer, e quando da noi specificato viene eseguito prima del Reducer (in pratica lo sostituisce) e genera una nuova Action con un nuovo “payload” ottenuto dal backend consumato da un altro Reducer, che effettivamente aggiornerà lo stato.
Ps. Nelle versioni precedenti di ngrx un Effect era chiamato Meta-Reducer (o meglio pre-meta-reducer o pre-middleware, in quanto eseguito prima del reducer).

Vediamo un esempio con una azione per cancellare una Company.

User click -> Action “DeleteCompanyAction(id: 2)” -> Effect “DeleteCompanyAction(id: 2)” con backend call + genera nuova Action “DeleteCompanyOKAction(id:2) -> Reducer “DeleteCompanyOKAction(id:2) ritorna lo Store senza la company con id 2 -> Store “companies = nuovo array” -> Observable “companies$” aggiornato.

In pratica la Action iniziale “DeleteCompanyAction” termina con l’Effect.

Giudizio finale

Questo pattern lo trovo molto utile e intuitivo, e l’implementazione molto semplice dopo aver capito le fondamenta. Direi un pattern obbligatorio per progetti di medio-grandi dimensioni. Il “contro” di questo pattern, è che richiede parecchie righe di codice in più (per creare le azioni, reducers e side effects) ma i “pro” sono notevoli nel lavoro in team, nella distribuzione delle responsabilità tra i vari componenti dell’applicazione, nella gestione dello stato con un “single point of truth”, possibilità di introdurre unit-test solo nei Reducers (la vera business-logic).

Consulenza

Ho notato che l’applicazione di questi concetti in un modo pragmatico non risulta semplice a tutti. Dalla mia esperienza l’errore più grande che si può commettere è di iniziare a implementare un pattern in un progetto reale, senza averlo ben chiaro, e ritrovarsi ad avere più problemi di prima. Ad esempio, pensate a un flusso logico di Login, come si applica Redux? E – ancora – se uso Redux, ma devo visualizzare un popup?

Rimango quindi a disposizione – come sempre – per consulenze su tutte le tematiche che riguardano sia il frontend che backend (ad esempio: Come realizzare applicazioni enterprise moderne). Leggo con piacere i commenti o le mail che mi inviate a info@stellarsolutions.it

Risorse aggiuntive

Per avere una spiegazione più dettagliata consiglio vivamente queste due risorse, in inglese:
(potete come sempre utilizzare google translate per la versione in italiano)

Spero di esservi stato utile e condividete pure la vostra esperienza nei commenti!

2 Comments

Add a Comment

Il tuo indirizzo email non sarà pubblicato.