Preambolo
Sentiamo sempre più parlare dell'uso crescente del concetto Redux con Angular (2+). Mentre cercavo diversi articoli che mi guidassero sull'argomento, mi sono reso conto che troppo pochi sono accessibili nella nostra bella lingua di Molière. Lo scopo di questo articolo è quindi quello di farvi capire Redux, e il suo utilizzo con un framework che non è il suo campo preferito. Se usi Angular e non hai familiarità con questo concetto, sei nel posto giusto.

Prerequisiti
Questo articolo è destinato agli sviluppatori che sono abituati a utilizzare Angular senza avere un'idea chiara di cosa sia Redux. Concetti come Osservabili, Nozioni di base sui dattiloscritti e Componenti saranno dati per scontati. Costituisce titolo preferenziale la conoscenza della programmazione funzionale.
Introduzione
Redux è un'architettura che mira a concentrare lo stato della tua applicazione in un unico posto. Le azioni verranno inviate dagli eventi, che ogni volta riprenderanno lo stato precedente, ne modificheranno parte e ne restituiranno uno nuovo che verrà utilizzato per gestire i tuoi dati.
Non è molto chiaro? Nessun problema, lo scopo di questo articolo è demistificare un concetto apparentemente complicato che si rivela semplice e logico.
Prima di entrare nel vivo della questione, vorrei insistere sul fatto che Redux, usato molto spesso con React, non dipende in alcun modo da quest'ultimo. È giunto il momento di comprendere i vantaggi di un'architettura molto pratica da utilizzare con qualsiasi framework/libreria orientata ai componenti e Angular ne è un esempio perfetto.
Quali sono i problemi con Angular che possono essere risolti da questa nuova gestione dei dati dell'app?
Variabili condivise da diversi componenti di diversi livelli
In un componente Angular in generale, è raro avere variabili in readonly per qualcosa di diverso dalle costanti di servizio o osservabili. Anche le variabili visualizzate direttamente nel componente vengono modificate da questo stesso componente. Ma dove puoi avere un vero problema è quando queste variabili vengono passate come Input a un sottocomponente e vengono modificate da questo sottocomponente. È possibile e funziona perché l'associazione dati di Angular è fatta apposta per questo. Tuttavia, diventa molto difficile testarlo e il codice può avere effetti collaterali.
Immagina un albero come:
Componente0 ├── Componente1 └── Componente2 └── Componente3
Abbiamo una variabile che vogliamo comune ai componenti 1 e 3. Quindi, dobbiamo dichiarare questa variabile come una proprietà di Component0 , solo per averla aggiornata in Component2 così come in Component3. Dovremo inserire un output nel componente 1 se vogliamo modificare la variabile a seguito di un evento. E fai lo stesso, non solo in Component3, ma anche in Component2 per salire a Component0 con EventEmitter.
Tanto codice, per modificare semplicemente una variabile usata in 2 posti nella stessa pagina.
Ovviamente, Angular incorpora già soluzioni che potremmo utilizzare in questo caso. L'uso particolare dei servizi, in cui memorizzeremmo la variabile condivisa. Se volessimo che fosse legato in entrambi i componenti, cioè cambiasse in tempo reale in Component1 quando è stato modificato in Component3, allora dovremmo usare paradigmi di programmazione reattiva, in particolare Osservabili con RXJS. Questo è fattibile e molte applicazioni funzionano in questo modo, ma può diventare difficile da mantenere quando lo schema si ripete regolarmente, in più punti dell'applicazione. Tuttavia, questa idea non dovrebbe essere completamente esclusa, poiché fornirà una base per la nostra soluzione.
I dati vincolati sono molto volatili e il debug può essere complicato
I dati possono cambiare continuamente e, a meno che tu non lavori costantemente nell'immutabilità, tenere traccia delle modifiche nei valori di un oggetto può diventare complicato. È anche difficile mantenere una cronologia delle azioni e degli eventi svolti per un po'. Redux fornisce anche una soluzione a questo livello con il suo "Debug del viaggio nel tempo". Redux mantiene ogni volta che viene creato un nuovo stato una copia della sua versione precedente e tutto è disponibile in gli strumenti di sviluppo tramite l'estensione

Può essere difficile organizzare il codice che modifica le variabili in modo puro.
Qui, entriamo davvero in parte della spiegazione della programmazione funzionale, ma una soluzione fornita da questo paradigma è il concetto di "funzione pura". Una funzione pura ha uno o più argomenti, non modifica alcuna variabile al di fuori del suo ambito e restituisce un valore che sarà sempre lo stesso con gli stessi argomenti. È abbastanza semplice in teoria e non sempre in pratica.
Se vuoi approfondire la spiegazione e la comprensione del paradigma della programmazione funzionale, posso solo consigliare un articolo di Yoan Ribeiro sull'argomento.
Inoltre, l'uso di Angular spesso ci porta a fare OOP piuttosto che programmazione funzionale, nel senso che modifichiamo i dati del componente (che è una classe) che sono per definizione esterni alle modalità di composizione.
Un po' di teoria prima della pratica
Ora che abbiamo sollevato alcuni punti inerenti allo sviluppo con Angular e che possono essere semplificati con Redux, cercheremo di capire bene lo schema, progettando un'applicazione Redux di base.

A volte vedrai Store, a volte State. Le due parole ricoprono globalmente lo stesso significato, ovvero lo stato dell'applicazione e la sua struttura dati.
Questo schema è molto semplice. Dal nostro componente, si parte inviare un'azione. Possiamo tradurre questo con il semplice fatto di inviare una chiamata, un evento che chiamerà esso stesso una funzione.
Quindi questa azione verrà applicata a a riduttore chi agirà sul stato dell'app. Alla fine è questo stesso stato che verrà letto dai componenti nell'interfaccia utente.
In due frasi e un semplice diagramma, potremmo riassumere il concetto abbastanza facilmente. Ma ovviamente le azioni, i riduttori e lo Stato hanno tutti un ruolo. E sebbene possano sembrare molti codici e funzioni per fare piccole cose, ognuno di essi ha un grande significato.
Come si può vedere, il flusso dovrà sempre essere unidirezionale, e questo è un punto importante che fungerà da pietra angolare della nostra architettura. Le azioni dell'utente passeranno attraverso il componente che invierà le azioni. Queste azioni verranno inviate ai riduttori, le nostre funzioni pure che restituiranno un nuovo stato per l'applicazione.
In precedenza, si parlava di una preoccupazione per gli effetti collaterali durante la modifica di variabili condivise da più componenti. Redux fornisce una vera soluzione a questo livello ponendosi sul concetto di programmazione funzionale, e non su quella orientata agli oggetti. Una riduzione al minimo degli effetti di bordo, ogni componente, qualunque esso sia, agirà solo sullo stato che è necessario per esso. Invieremo un'azione per aggiornare una proprietà dello stato. Quindi in ogni punto in cui questa proprietà verrà utilizzata sotto forma di variabile in un componente, verrà vincolata e quindi modificata, il tutto in un'azione specifica che avrà solo modificato questa proprietà.
Esempio di riduttore
const reducer: Reducer<AppState> = (state: AppState, action: Action) => {
switch (action.type) {
case "IS_LOADING":
return {
...state,
isLoading: action.payload
};
default:
return state;
}
};
Largo alla pratica
Ecco un diagramma leggermente più complicato che traduce un caso d'uso reale nell'ambito dello sviluppo di un'applicazione.

NDD: un payload è il nome dato a una variabile di qualsiasi tipo che verrà inviata all'azione e utilizzata dal riduttore.
Prendiamo un caso in cui costruiamo un'applicazione che sembra un blog, ma si occupa di manga (per cambiare gli articoli).
Avremo un componente che visualizzerà i dati di un manga tramite il suo ID (presente nell'URL). Immaginiamo che il titolo del manga, il suo autore e il suo numero di volumi siano dati caricati quando viene visualizzata la pagina, ma non la sua descrizione. Essendo questo molto lungo, devi cliccare su un pulsante per visualizzarlo e quindi caricarlo.
Quando si fa clic su questo pulsante, accadranno diverse cose. Il nostro componente invierà un'azione che caricherà la descrizione del manga tramite la funzione caricaDescrizioneManga(). Questa funzione lo farà prima di tutto spedire l'azione DESCRIZIONE_IS_LOADING con carico utile su true. Vorremo mostrare all'utente che le informazioni stanno caricando. Possiamo ad esempio, fintanto che questo valore è vero, visualizzare uno spinner. La componente recupererà quindi tale valore dallo Stato, che è stato aggiornato dal riduttore chiamato dall'azione DESCRIZIONE_IS_LOADING.
Una volta in modo totalmente sincrono, abbiamo aggiornato il nostro Stato per dire che la descrizione si sta caricando, possiamo andare avanti. Il componente non solo ha inviato questa azione, ma ha anche chiamato un servizio che recupererà la descrizione tramite una chiamata HTTP. Chi dice HTTP qui dice asincrono, quindi non sappiamo quando arriverà la risposta. Questo è solitamente il motivo per cui vengono visualizzati gli spinner, come in questo esempio.
Quando la richiesta ritorna, il servizio restituirà il risultato al componente che potrà finalmente avviare le due azioni seguenti (collegandosi al risultato tramite Promise o Subscription). Il componente sarà quindi in grado di inviare nuovamente l'azione DESCRIZIONE_IS_LOADING questa volta con un carico utile a falso.
Ma non è tutto. L'obiettivo della nostra operazione, dal componente, è recuperare la descrizione del manga. Va bene mettere uno spinner durante il caricamento e rimuoverlo una volta ricevuti i dati, ma sarebbe comunque necessario utilizzare quei dati. Per questo, invieremo una seconda azione dopo la prima che imposta il caricamento su false, che sarà CARICA_DESCRIZIONE. Questa azione conterrà nel payload i dati che vogliamo inviare al riduttore quindi trovare nello stato. Sono questi dati che verranno quindi visualizzati sull'interfaccia utente, fornita al componente.
Il codice prodotto per questo esempio si trova qui Archivio Github per ottenere una comprensione più profonda dell'uso di Redux oltre la teoria e gli schemi. È relativamente semplice e corrisponde a un avvio di NgRedux, con soprattutto gli elementi essenziali per il funzionamento dell'architettura.
Le epopee
Un altro concetto portato da Redux è quello di Epics. Nel caso precedente, vorremmo ad esempio non essere obbligati a specificare che una volta effettuata la chiamata di servizio e inviata l'azione, vogliamo anche che il caricatore scompaia inviando una nuova azione. Se dovessimo inviare l'azione che riempie i dati in più punti, dovremmo duplicare il codice per far scomparire il caricatore. È qui che entrano in gioco le Epopee. Un'epica è una funzione che verrà attivata da un riduttore e che richiederà un flusso di azioni per restituire un altro flusso di azioni. Nel nostro caso, basti dire che ogni volta l'azione CARICA_DESCRIZIONE viene inviato, vogliamo anche inviare l'azione DESCRIZIONE_IS_LOADING con il carico utile impostato su false. Semplice ed efficiente, nessuna duplicazione del codice o casi aggiuntivi da gestire.
Maggiori informazioni sul concetto di Epics e sui loro casi d'uso:
- https://medium.com/kevin-salters-blog/epic-middleware-in-redux-e4385b6ff7c6
- https://redux-observable.js.org/docs/basics/Epics.html
Angolare-Redux
Fino ad allora, rimane ancora una domanda. Sappiamo come ascoltare gli eventi sul componente per inviare le nostre azioni. Sappiamo come collegare un servizio ad esso se abbiamo bisogno di dati asincroni e come inviare in modo efficiente le nostre azioni ai riduttori in modo che i cambiamenti di stato dei dati non entrino in conflitto. Tranne che una volta aggiornato il nostro stato, come recuperare gli elementi è ancora un po' poco chiaro.
Come fai a sapere quando una variabile ha cambiato valore? E come utilizzare questo nuovo valore nel nostro componente?
È per rispondere a questo tipo di domande che piacciono alle biblioteche Angolare-Redux. Questo è un middleware che ci consentirà di agire e recuperare elementi dal nostro stato in modo statico o dinamico. Ecco un codice di esempio per recuperare la descrizione del nostro manga in modo dinamico.
import { select } from '@angular-redux/store';
import { Observable } from 'rxjs/Observable';
import { MangaState } from '../store/state';
@Component({
selector: 'app-manga',
})
export class MangaComponent {
@select(['manga']) readonly manga: Observable;
constructor() {}
}
Preso dal codice del repository sopra e semplificato, questo codice ci consente di avere una panoramica del recupero dei nostri dati dal negozio.
La prima parola chiave nella riga è the @Selezionare che è un decoratore fornito da angular-redux e che ci consente di recuperare il nostro elemento manga dal negozio e renderlo un Osservabile. A cosa corrisponde il tipo della nostra variabile, prendendo come input l'interfaccia MangaState che corrisponde al tipo del nostro oggetto.
Da lì, sarà facile visualizzare i dati in HTML. Tuttavia, c'è una piccola modifica da fare per gestire l'Osservabile.
<div *ngIf="manga | async; let manga">
Nom du manga : {{ manga.name }}
Auteur du manga : {{ manga.author }}
<div>LOADING DESCRIPTION</div>
<div>{{ manga.description }}</div>
</div>
Come puoi vedere, devi gestire l'Osservabile in modo asincrono. L'Osservabile è un flusso che può avere valori fissi in un dato momento, ma per visualizzare questi valori nell'HTML, devi mettere una pipe | asincrono che recupererà automaticamente l'ultimo valore. Seconda osservazione, dietro la pipe, puoi creare una variabile locale in modo da non dover eseguire una pipe asincrona per visualizzare ogni valore (cosa possibile). Così il {{ manga.name }} visualizzerà effettivamente la proprietà name della variabile locale manga creata, non quella di Observable.
Feedback dall'esperienza
Avendo lavorato su applicazioni Angular da zero con e senza Redux, direi che c'è un talento per "pensare Redux", ma ciò accade abbastanza rapidamente. Come quando si passa da un linguaggio a oggetti a un linguaggio funzionale, è necessario abituarsi a codificare in modo diverso. Ma il gioco vale sicuramente la pena e la combinazione di Redux + digitazione + test è una combinazione vincente per un'applicazione estremamente solido, facilmente debuggabile e manutenibile. Ovviamente la sua implementazione richiede più tempo perché uno Store deve essere ben pensato in base alle esigenze dell'applicazione. La separazione dei dati tra diversi riduttori può anche essere un punto che determinerà la manutenibilità a seconda del riutilizzo di questi riduttori da pagine diverse. Redux è un'architettura aperta in cui puoi persino aggiungere schemi corrispondenti alle tue esigenze, come sostituire Epics con un altro sistema di Dispatcher che avrà la responsabilità esclusiva per l'invio delle azioni, ad esempio.
Conclusione
Vediamo che un'architettura come Redux può interfacciarsi con qualsiasi tipo di applicazione. Devi solo comprendere appieno l'utilità del modello per la gestione dei dati e lo stato globale della tua applicazione.
Devi solo pensare a degli extra per integrarlo in Angular, come Angular-Redux per gestire il legame tra le variabili dei componenti e lo stato globale.
L'uso di Typescript assume il suo pieno significato, in particolare attraverso la digitazione del negozio e dei dati in esso contenuti, che si trovano nei riduttori oltre che nelle azioni, e anche negli Osservabili nelle componenti (vedere il codice dal repository). Abbiamo quindi su tutto il codice una sicurezza apportata dalla digitazione per evitare gli errori il più possibile.
Un altro punto importante: Redux non è magia. Ovviamente, devi renderti conto che Redux non è altro che un'architettura aggiunta alla tua applicazione, ma non elimina magicamente tutti i problemi di pattern che la tua applicazione potrebbe già avere. Anche se la libreria fornisce una soluzione efficace per la gestione dei dati dell'applicazione, resta il fatto che la sua implementazione richiede tempo. Bisogna pensare con attenzione alla struttura dei dati e alla sua normalizzazione, perché questo avrà un impatto significativo sull'architettura delle azioni, dello store e dei riduttori. Devi prenderti il tempo necessario per digitare e testare questi dati, nonché le funzioni che li modificano per mantenere un'applicazione sana, funzionale e senza sorprese.
scritto da Guglielmo Barranco
