Un'analisi approfondita dei vantaggi e delle caratteristiche di NgRx
Pubblicato: 2022-03-11Se un team leader istruisce uno sviluppatore a scrivere molto codice standard invece di scrivere alcuni metodi per risolvere un determinato problema, ha bisogno di argomenti convincenti. Gli ingegneri del software sono risolutori di problemi; preferiscono automatizzare le cose ed evitare inutili boilerplate.
Anche se NgRx viene fornito con del codice standard, fornisce anche potenti strumenti per lo sviluppo. Questo articolo dimostra che dedicare un po' più di tempo alla scrittura del codice produrrà vantaggi che ne valgono la pena.
La maggior parte degli sviluppatori ha iniziato a utilizzare la gestione dello stato quando Dan Abramov ha rilasciato la libreria Redux. Alcuni hanno iniziato a usare la gestione statale perché era una tendenza, non perché gli mancasse. Gli sviluppatori che utilizzano un progetto standard "Hello World" per la gestione dello stato potrebbero ritrovarsi rapidamente a scrivere lo stesso codice più e più volte, aumentando la complessità senza alcun vantaggio.
Alla fine, alcuni divennero frustrati e abbandonarono completamente la gestione statale.
Il mio problema iniziale con NgRx
Penso che questa preoccupazione standard fosse un grosso problema con NgRx. All'inizio, non siamo stati in grado di vedere il quadro generale dietro di esso. NgRx è una libreria, non un paradigma di programmazione o una mentalità. Tuttavia, per cogliere appieno la funzionalità e l'usabilità di questa libreria, dobbiamo ampliare un po' le nostre conoscenze e concentrarci sulla programmazione funzionale. È allora che potresti scrivere codice standard e sentirti felice. (Dico sul serio.) Una volta ero uno scettico di NgRx; ora sono un ammiratore di NgRx.
Qualche tempo fa, ho iniziato a utilizzare la gestione dello stato. Ho vissuto l'esperienza standard sopra descritta, quindi ho deciso di smettere di usare la libreria. Dato che amo JavaScript, cerco di ottenere almeno una conoscenza fondamentale su tutti i framework popolari in uso oggi. Ecco cosa ho imparato usando React.
React ha una funzione chiamata Hooks. Proprio come i componenti in Angular, gli Hook sono semplici funzioni che accettano argomenti e restituiscono valori. Un hook può avere uno stato, chiamato effetto collaterale. Quindi, ad esempio, un semplice pulsante in Angular potrebbe essere tradotto in React in questo modo:
@Component({ selector: 'simple-button', template: ` <button>Hello {{ name }}</button> `, }) export class SimpleButtonComponent { @Input() name!: string; } export default function SimpleButton(props: { name: string }) { return <button>{props.name} </button>; }Come puoi vedere, questa è una trasformazione semplice:
- SimpleButtonComponent => SimpleButton
- @Input() nome => props.name
- modello => valore di ritorno
La nostra funzione React SimpleButton ha una caratteristica importante nel mondo della programmazione funzionale: è una funzione pura . Se stai leggendo questo, presumo che tu abbia sentito quel termine almeno una volta. NgRx.io cita due volte le funzioni pure nei concetti chiave:
- I cambiamenti di stato sono gestiti da funzioni pure denominate
reducersche prendono lo stato corrente e l'ultima azione per calcolare un nuovo stato. - I selettori sono pure funzioni utilizzate per selezionare, derivare e comporre pezzi di stato.
In React, gli sviluppatori sono incoraggiati a utilizzare gli Hooks come funzioni pure il più possibile. Angular incoraggia anche gli sviluppatori a implementare lo stesso modello utilizzando il paradigma Smart-Dumb Component.
Fu allora che mi resi conto che mi mancavano alcune abilità cruciali di programmazione funzionale. Non ci è voluto molto per capire NgRx, poiché dopo aver appreso i concetti chiave della programmazione funzionale, ho avuto un "Aha! momento”: avevo migliorato la mia comprensione di NgRx e volevo usarlo di più per capire meglio i vantaggi che offre.
Questo articolo condivide la mia esperienza di apprendimento e le conoscenze acquisite su NgRx e sulla programmazione funzionale. Non spiego l'API per NgRx o come chiamare le azioni o utilizzare i selettori. Invece, condivido il motivo per cui ho imparato ad apprezzare che NgRx è un'ottima libreria: non è solo una tendenza relativamente nuova, ma offre una serie di vantaggi.
Iniziamo con la programmazione funzionale .
Programmazione Funzionale
La programmazione funzionale è un paradigma che differisce notevolmente da altri paradigmi. Questo è un argomento molto complesso con molte definizioni e linee guida. Tuttavia, la programmazione funzionale contiene alcuni concetti fondamentali e conoscerli è un prerequisito per padroneggiare NgRx (e JavaScript in generale).
Questi concetti fondamentali sono:
- Pura funzione
- Stato immutabile
- Effetto collaterale
Ripeto: è solo un paradigma, niente di più. Non esiste una libreria funzionale.js che scarichiamo e utilizziamo per scrivere software funzionale. È solo un modo di pensare alla scrittura di applicazioni. Partiamo dal concetto chiave più importante: la pura funzione .
Pura funzione
Una funzione è considerata una funzione pura se segue due semplici regole:
- Il passaggio degli stessi argomenti restituisce sempre lo stesso valore
- Mancanza di un effetto collaterale osservabile coinvolto all'interno dell'esecuzione della funzione (un cambiamento di stato esterno, chiamata di un'operazione di I/O, ecc.)
Quindi una funzione pura è solo una funzione trasparente che accetta alcuni argomenti (o nessun argomento) e restituisce un valore atteso. Hai la certezza che chiamare questa funzione non comporterà effetti collaterali, come il collegamento in rete o la modifica di uno stato utente globale.
Diamo un'occhiata a tre semplici esempi:
//Pure function function add(a,b){ return a + b; } //Impure function breaking rule 1 function random(){ return Math.random(); } //Impure function breaking rule 2 function sayHello(name){ console.log("Hello " + name); }- La prima funzione è pura perché restituirà sempre la stessa risposta quando si passano gli stessi argomenti.
- La seconda funzione non è pura perché non deterministica e restituisce risposte diverse ogni volta che viene chiamata.
- La terza funzione non è pura perché usa un effetto collaterale (chiamando
console.log).
È facile discernere se la funzione è pura o meno. Perché una funzione pura è meglio di una impura? Perché è più semplice pensarci. Immagina di leggere del codice sorgente e di vedere una chiamata di funzione che sai essere pura. Se il nome della funzione è corretto, non è necessario esplorarlo; sai che non cambia nulla, restituisce ciò che ti aspetti. È fondamentale per il debug quando si dispone di un'enorme applicazione aziendale con molta logica aziendale, poiché può essere un enorme risparmio di tempo.
Inoltre, è semplice da testare. Non devi iniettare nulla o deridere alcune funzioni al suo interno, basta passare argomenti e verificare se il risultato è una corrispondenza. C'è una forte connessione tra il test e la logica: se un componente è facile da testare, è facile capire come e perché funziona.
Le funzioni pure sono dotate di una funzionalità molto pratica e adatta alle prestazioni chiamata memorizzazione. Se sappiamo che chiamare gli stessi argomenti restituirà lo stesso valore, possiamo semplicemente memorizzare nella cache i risultati e non perdere tempo a richiamarlo di nuovo. NgRx si trova sicuramente in cima alla memorizzazione; questo è uno dei motivi principali per cui è veloce.
Potresti chiederti: "E gli effetti collaterali? Dove vanno?" Nel suo discorso GOTO, Russ Olsen scherza sul fatto che i nostri clienti non ci pagano per funzioni pure, ci pagano per effetti collaterali. È vero: a nessuno importa della funzione pura Calcolatrice se non viene stampata da qualche parte. Gli effetti collaterali hanno il loro posto nell'universo della programmazione funzionale. Lo vedremo a breve.
Per ora, passiamo al passaggio successivo nel mantenimento di architetture di applicazioni complesse, il prossimo concetto fondamentale: stato immutabile .
Stato immutabile
C'è una semplice definizione per uno stato immutabile:
- Puoi solo creare o eliminare uno stato. Non puoi aggiornarlo.
In parole povere, per aggiornare l'età di un oggetto Utente... :
let user = { username:"admin", age:28 }… dovresti scriverlo così:
// Not like this newUser.age = 30; // But like this let newUser = {...user, age:29 }Ogni modifica è un nuovo oggetto che ha copiato le proprietà da quelle precedenti. In quanto tale, siamo già in una forma di stato immutabile.
String, Boolean e Number sono tutti stati immutabili: non è possibile aggiungere o modificare valori esistenti. Al contrario, una data è un oggetto mutevole: manipoli sempre lo stesso oggetto data.
L'immutabilità si applica a tutta l'applicazione: se si passa un oggetto utente all'interno della funzione che ne modifica l'età, non dovrebbe modificare un oggetto utente, dovrebbe creare un nuovo oggetto utente con un'età aggiornata e restituirlo:
function updateAge(user, age) { return {...user, age: age) } let user = {username: 'admin', age: 29}; let newUser = updateAge(user, 32);Perché dovremmo dedicare tempo e attenzione a questo? Ci sono un paio di vantaggi che vale la pena sottolineare.
Un vantaggio per i linguaggi di programmazione back-end riguarda l'elaborazione parallela. Se un cambio di stato non dipende da un riferimento e ogni aggiornamento è un nuovo oggetto, puoi dividere il processo in blocchi e lavorare sulla stessa attività con innumerevoli thread senza condividere la stessa memoria. Puoi anche parallelizzare le attività tra i server.
Per framework come Angular e React, l'elaborazione parallela è uno dei modi più vantaggiosi per migliorare le prestazioni dell'applicazione. Ad esempio, Angular deve controllare le proprietà di ogni oggetto che passi tramite i binding di input per discernere se un componente deve essere renderizzato o meno. Ma se impostiamo ChangeDetectionStrategy.OnPush invece del valore predefinito, verificherà per riferimento e non per ciascuna proprietà. In una grande applicazione, questo fa sicuramente risparmiare tempo. Se aggiorniamo il nostro stato in modo immutabile, otteniamo questo aumento delle prestazioni gratuitamente.
L'altro vantaggio per uno stato immutabile condiviso da tutti i linguaggi di programmazione e dai framework è simile ai vantaggi delle funzioni pure: è più facile pensare e testare. Quando un cambiamento è un nuovo stato nato da uno vecchio, sai esattamente su cosa stai lavorando e puoi monitorare esattamente come e dove lo stato è cambiato. Non perdi la cronologia degli aggiornamenti e puoi annullare/ripristinare le modifiche per lo stato (React DevTools è un esempio).
Tuttavia, se un singolo stato viene aggiornato, non conoscerai la cronologia di tali modifiche. Pensa a uno stato immutabile come la cronologia delle transazioni per un conto bancario. È praticamente un must.
Ora che abbiamo esaminato l'immutabilità e la purezza, affrontiamo il restante concetto centrale: effetto collaterale .
Effetto collaterale
Possiamo generalizzare la definizione di effetto collaterale:
- In informatica, si dice che un'operazione, una funzione o un'espressione abbia un effetto collaterale se modifica alcuni valori di variabili di stato al di fuori del suo ambiente locale. Vale a dire che ha un effetto osservabile oltre a restituire un valore (l'effetto principale) all'invocatore dell'operazione.
In poche parole, tutto ciò che modifica uno stato al di fuori dell'ambito della funzione, tutte le operazioni di I/O e parte del lavoro che non è direttamente connesso alla funzione, può essere considerato un effetto collaterale. Tuttavia, dobbiamo evitare di utilizzare effetti collaterali all'interno di funzioni pure perché gli effetti collaterali contraddicono la filosofia di programmazione funzionale. Se si utilizza un'operazione di I/O all'interno di una funzione pura, smette di essere una funzione pura.
Tuttavia, dobbiamo avere degli effetti collaterali da qualche parte, poiché un'applicazione senza di essi sarebbe inutile. In Angular, non solo le funzioni pure devono essere protette dagli effetti collaterali, ma dobbiamo anche evitare di usarle in Componenti e Direttive.
Esaminiamo come possiamo implementare la bellezza di questa tecnica all'interno del framework Angular.
Programmazione angolare funzionale
Una delle prime cose da capire su Angular è la necessità di disaccoppiare i componenti in componenti più piccoli il più spesso possibile per facilitare la manutenzione e il test. Questo è necessario, poiché dobbiamo dividere la nostra logica aziendale. Inoltre, gli sviluppatori Angular sono incoraggiati a lasciare i componenti solo per scopi di rendering e spostare tutta la logica aziendale all'interno dei servizi.
Per espandere questi concetti, gli utenti Angular hanno aggiunto il modello "Dumb-Smart Component" al loro vocabolario. Questo modello richiede che le chiamate di servizio non esistano all'interno dei componenti di piccole dimensioni. Poiché la logica aziendale risiede nei servizi, dobbiamo comunque chiamare questi metodi di servizio, attendere la loro risposta e solo dopo apportare modifiche allo stato. Quindi, i componenti hanno una logica comportamentale al loro interno.
Per evitarlo, possiamo creare uno Smart Component (Root Component), che contiene la logica aziendale e di comportamento, passare gli stati tramite Input Properties e richiamare azioni in ascolto dei parametri Output. In questo modo, i piccoli componenti sono davvero solo a scopo di rendering. Naturalmente, il nostro componente principale deve avere alcune chiamate di servizio al suo interno e non possiamo semplicemente rimuoverle, ma la sua utilità sarebbe limitata solo alla logica aziendale, non al rendering.
Diamo un'occhiata a un esempio di componente contatore. Un contatore è un componente che ha due pulsanti che aumentano o diminuiscono il valore e un displayField che visualizza il currentValue . Quindi finiamo con quattro componenti:
- Controcontainer
- Pulsante Aumenta
- Pulsante Riduci
- Valore corrente
Tutta la logica risiede all'interno di CounterContainer , quindi tutti e tre sono solo renderer. Ecco il codice per loro tre:
@Component({ selector: 'decrease-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Decrease </button>`, }) export class DecreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); } @Component({ selector: 'current-value', template: `<button> {{ currentValue }} </button>`, }) export class CurrentValueComponent { @Input() currentValue!: string; } @Component({ selector: 'increase-button', template: `<button (click)="increase.emit()" [disabled]="disabled"> Increase </button>`, }) export class IncreaseButtonComponent { @Input() disabled!: boolean; @Output() increase = new EventEmitter(); }Guarda come sono semplici e puri. Non hanno stato o effetti collaterali, dipendono solo dalle proprietà di input e dagli eventi di emissione. Immagina quanto sia facile testarli. Possiamo chiamarli componenti puri perché è quello che sono veramente. Dipendono solo dai parametri di input, non hanno effetti collaterali e restituiscono sempre lo stesso valore (stringa modello) passando gli stessi parametri.

Quindi le funzioni pure nella programmazione funzionale vengono trasferite nei componenti puri in Angular. Ma dove va tutta la logica? La logica è ancora lì ma in un posto leggermente diverso, ovvero il CounterComponent .
@Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { @Input() disabled!: boolean; currentValue = 0; get decreaseIsDisabled() { return this.currentValue === 0; } get increaseIsDisabled() { return this.currentValue === 100; } constructor() {} ngOnInit(): void {} decrease() { this.currentValue -= 1; } increase() { this.currentValue += 1; } } Come puoi vedere, la logica di comportamento vive in CounterContainer ma manca la parte di rendering (dichiara i componenti all'interno del modello) perché la parte di rendering è per componenti puri.
Potremmo iniettare tutto il servizio che vogliamo perché gestiamo tutte le manipolazioni dei dati e le modifiche di stato qui. Una cosa degna di nota è che se abbiamo un componente annidato in profondità, non dobbiamo creare un solo componente a livello di radice. Possiamo dividerlo in componenti intelligenti più piccoli e utilizzare lo stesso schema. In definitiva, dipende dalla complessità e dal livello nidificato di ciascun componente.
Possiamo facilmente passare da quel modello alla libreria NgRx stessa, che è solo uno strato sopra di esso.
Libreria NgRx
Possiamo dividere qualsiasi applicazione web in tre parti principali:
- Logica di business
- Stato dell'applicazione
- Logica di rendering
La logica aziendale è tutto il comportamento che sta accadendo all'applicazione, come rete, input, output, API, ecc.
Lo stato dell'applicazione è lo stato dell'applicazione. Può essere globale, come utente attualmente autorizzato, e anche locale, come valore del componente contatore corrente.
Rendering Logic comprende il rendering, come la visualizzazione di dati tramite DOM, la creazione o la rimozione di elementi e così via.
Usando il pattern Dumb-Smart abbiamo disaccoppiato Rendering Logic da Business Logic e Application State ma possiamo anche dividerli perché sono entrambi concettualmente diversi l'uno dall'altro. Lo stato dell'applicazione è come un'istantanea della tua app nell'ora corrente. Business Logic è come una funzionalità statica che è sempre presente nella tua app. Il motivo più importante per dividerli è che la Business Logic è principalmente un effetto collaterale che vogliamo evitare il più possibile nel codice dell'applicazione. Questo è quando la libreria NgRx, con il suo paradigma funzionale, brilla.
Con NgRx si disaccoppiano tutte queste parti. Ci sono tre parti principali:
- Riduttori
- Azioni
- Selettori
Insieme alla programmazione funzionale, tutti e tre si combinano per darci un potente strumento per gestire applicazioni di qualsiasi dimensione. Esaminiamo ciascuno di essi.
Riduttori
Un riduttore è una funzione pura, che ha una firma semplice. Prende un vecchio stato come parametro e restituisce un nuovo stato, derivato dal vecchio o da uno nuovo. Lo stato stesso è un singolo oggetto, che vive con il ciclo di vita dell'applicazione. È come un tag HTML, un singolo oggetto radice.
Non è possibile modificare direttamente un oggetto di stato, è necessario modificarlo con riduttori. Ciò ha una serie di vantaggi:
- La logica del cambiamento dello stato vive in un unico luogo e tu sai dove e come cambia lo stato.
- Le funzioni del riduttore sono pure funzioni, facili da testare e gestire.
- Poiché i riduttori sono funzioni pure, possono essere memorizzati, consentendo di memorizzarli nella cache ed evitare calcoli aggiuntivi.
- I cambiamenti di stato sono immutabili. Non aggiorni mai la stessa istanza. Invece, ne restituisci sempre uno nuovo. Ciò consente un'esperienza di debug del "viaggio nel tempo".
Questo è un banale esempio di riduttore:
function usernameReducer(oldState, username) { return {...oldState, username} }Anche se è un riduttore fittizio molto semplice, è lo scheletro di tutti i riduttori lunghi e complessi. Tutti condividono gli stessi vantaggi. Potremmo avere centinaia di riduttori nella nostra applicazione e possiamo farne quanti ne vogliamo.
Per il nostro componente contatore, il nostro stato e i riduttori potrebbero assomigliare a questo:
interface State{ decreaseDisabled:boolean; increaseDisabled:boolean; currentValue:number; } const MIN_VALUE=0; const MAX_VALUE =100; function decreaseReducer(oldState) { const newValue = oldState.currentValue -1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MIN_VALUE } function increaseReducer(oldState) { const newValue = oldState.currentValue + 1 return {...oldState,currentValue : newValue, decreaseDisabled: newValue===MAX_VALUE }Abbiamo rimosso lo stato dal componente. Ora abbiamo bisogno di un modo per aggiornare il nostro stato e chiamare il riduttore appropriato. È allora che entrano in gioco le azioni.
Azioni
Un'azione è un modo per notificare a NgRx di chiamare un riduttore e aggiornare lo stato. Senza quello, non avrebbe senso usare NgRx. Un'azione è un semplice oggetto che associamo al riduttore di corrente. Dopo averlo chiamato, verrà chiamato il riduttore appropriato, quindi nel nostro esempio potremmo avere le seguenti azioni:
enum CounterActions { IncreaseValue = '[Counter Component] Increase Value', DecreaseValue = '[Counter Component] Decrease Value', } on(CounterActions.IncreaseValue,increaseReducer); on(CounterActions.DecreaseValue,decreaseReducer);Le nostre azioni sono legate ai riduttori. Ora possiamo modificare ulteriormente il nostro componente contenitore e chiamare le azioni appropriate quando necessario:
@Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="decreaseIsDisabled" (decrease)="decrease()"> </decrease-button> <current-value [currentValue]="currentValue"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled"> </increase-button> `, }) export class CounterContainerComponent implements OnInit { constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }Nota: abbiamo rimosso lo stato e lo aggiungeremo a breve .
Ora il nostro CounterContainer non ha alcuna logica di cambio di stato. Sa solo cosa inviare. Ora abbiamo bisogno di un modo per visualizzare questi dati nella vista. Questa è l'utilità dei selettori.
Selettori
Un selettore è anche una funzione pura molto semplice, ma a differenza di un riduttore, non aggiorna lo stato. Come suggerisce il nome, il selettore si limita a selezionarlo. Nel nostro esempio, potremmo avere tre semplici selettori:
function selectCurrentValue(state) { return state.currentValue; } function selectDicreaseIsDisabled(state) { return state.decreaseDisabled; } function selectIncreaseIsDisabled(state) { return state.increaseDisabled; } Usando questi selettori, potremmo selezionare ogni sezione di uno stato all'interno del nostro componente CounterContainer intelligente.
@Component({ selector: 'counter-container', template: ` <decrease-button [disabled]="ecreaseIsDisabled$ | async" (decrease)="decrease()" > </decrease-button> <current-value [currentValue]="currentValue$ | async"> </current-value> <increase-button (increase)="increase()" [disabled]="increaseIsDisabled$ | async" > </increase-button> `, }) export class CounterContainerComponent implements OnInit { decreaseIsDisabled$ = this.store.select(selectDicreaseIsDisabled); increaseIsDisabled$ = this.store.select(selectIncreaseIsDisabled); currentValue$ = this.store.select(selectCurrentValue); constructor(private store: Store<any>) {} decrease() { this.store.dispatch(CounterActions.DicreaseValue); } increase() { this.store.dispatch(CounterActions.IncreaseValue); } }Queste selezioni sono asincrone per impostazione predefinita (come lo sono gli osservabili in generale). Questo non ha importanza, almeno dal punto di vista del modello. Lo stesso sarebbe vero per uno sincrono, poiché selezioniamo semplicemente qualcosa dal nostro stato.
Facciamo un passo indietro e guardiamo il quadro generale per vedere cosa abbiamo ottenuto finora. Abbiamo una Counter Application, che ha tre parti principali che sono quasi disaccoppiate l'una dall'altra. Nessuno sa come si gestisce lo stato dell'applicazione o come il livello di rendering esegue il rendering dello stato.
Le parti disaccoppiate utilizzano il bridge (Azioni, Selettori) per connettersi tra loro. Sono disaccoppiati a tal punto che potremmo prendere l'intero codice dell'applicazione di stato e spostarlo in un altro progetto, ad esempio per una versione mobile. L'unica cosa che dovremmo implementare sarebbe il rendering. Ma per quanto riguarda i test?
A mio modesto parere, il test è la parte migliore di NgRx. Testare questo progetto di esempio è come giocare a tris. Ci sono solo funzioni pure e componenti puri, quindi testarli è un gioco da ragazzi. Ora immagina se questo progetto diventa più grande, con centinaia di componenti. Se seguiamo lo stesso schema, vorremmo semplicemente aggiungere sempre più pezzi insieme. Non diventerebbe un blob disordinato e illeggibile di codice sorgente.
Abbiamo quasi finito. C'è solo una cosa importante da coprire: gli effetti collaterali. Ho menzionato molte volte gli effetti collaterali finora, ma mi sono fermato prima di spiegare dove conservarli.
Questo perché gli effetti collaterali sono la ciliegina sulla torta e costruendo questo modello, è molto facile rimuoverli dal codice dell'applicazione.
Effetti collaterali
Diciamo che la nostra Counter Application ha un timer e ogni tre secondi aumenta automaticamente il valore di uno. Questo è un semplice effetto collaterale, che deve vivere da qualche parte. È lo stesso effetto collaterale, per definizione, di una richiesta Ajax.
Se pensiamo agli effetti collaterali, la maggior parte ha due ragioni principali per esistere:
- Fare qualsiasi cosa al di fuori dell'ambiente statale
- Aggiornamento dello stato dell'applicazione
Ad esempio, la memorizzazione di uno stato all'interno di LocalStorage è la prima opzione, mentre l'aggiornamento dello stato dalla risposta Ajax è la seconda. Ma entrambi condividono la stessa firma: ogni effetto collaterale deve avere un punto di partenza. Deve essere chiamato almeno una volta per richiedere l'avvio dell'azione.
Come accennato in precedenza, NgRx ha un ottimo strumento per dare un comando a qualcuno. Questa è un'azione. Potremmo chiamare qualsiasi effetto collaterale inviando un'azione. Lo pseudocodice potrebbe assomigliare a questo:
function startTimer(){ setInterval(()=>{ console.log("Hello application"); },3000) } on(CounterActions.StartTime,startTimer) ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);È abbastanza banale. Come accennato in precedenza, gli effetti collaterali aggiornano qualcosa o meno. Se un effetto collaterale non aggiorna nulla, non c'è niente da fare; lo lasciamo e basta. Ma se vogliamo aggiornare uno stato, come lo facciamo? Allo stesso modo un componente tenta di aggiornare uno stato: chiamando un'altra azione. Quindi chiamiamo un'azione all'interno dell'effetto collaterale, che aggiorna lo stato:
function startTimer(store) { setInterval(()=> { // We are dispatching another action dispatch(CounterActions.IncreaseValue) }, 3000) } on(CounterActions.StartTime, startTimer); ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);Ora abbiamo un'applicazione completamente funzionale.
Riassumendo la nostra esperienza NgRx
Ci sono alcuni argomenti importanti che vorrei menzionare prima di finire il nostro viaggio NgRx:
- Il codice mostrato è un semplice pseudo codice che ho inventato per l'articolo; è adatto solo a scopo dimostrativo. NgRx è il luogo in cui vivono le vere fonti.
- Non esiste una linea guida ufficiale che dimostri la mia teoria sulla connessione della programmazione funzionale con la libreria NgRx. È solo la mia opinione formata dopo aver letto dozzine di articoli ed esempi di codice sorgente creati da persone altamente qualificate.
- Dopo aver usato NgRx ti renderai sicuramente conto che è molto più complesso di questo semplice esempio. Il mio obiettivo non era farlo sembrare più semplice di quanto non sia in realtà, ma mostrarti che anche se è un po' complesso e potrebbe anche comportare un percorso più lungo verso la destinazione, vale lo sforzo aggiuntivo.
- L'utilizzo peggiore di NgRx è utilizzarlo ovunque, indipendentemente dalle dimensioni o dalla complessità dell'applicazione. Ci sono alcuni casi in cui non dovresti usare NgRx; ad esempio, nei moduli. È quasi impossibile implementare moduli all'interno di NgRx. I moduli sono incollati al DOM stesso; non possono vivere separatamente. Se provi a disaccoppiarli, ti ritroverai a odiare non solo NgRx ma la tecnologia web in generale.
- A volte l'utilizzo dello stesso codice standard, anche per un piccolo esempio, può trasformarsi in un incubo, anche se può avvantaggiarci in futuro. In tal caso, basta integrare con un'altra straordinaria libreria, che fa parte dell'ecosistema NgRx (ComponentStore).
