O scufundare profundă în avantajele și caracteristicile NgRx
Publicat: 2022-03-11Dacă un lider de echipă instruiește un dezvoltator să scrie o mulțime de cod standard în loc să scrie câteva metode pentru a rezolva o anumită problemă, ei au nevoie de argumente convingătoare. Inginerii software rezolvă probleme; ei preferă să automatizeze lucrurile și să evite boilerplate inutile.
Chiar dacă NgRx vine cu un cod standard, oferă și instrumente puternice pentru dezvoltare. Acest articol demonstrează că petrecerea unui pic mai mult timp scriind cod va aduce beneficii care îl fac să merite efortul.
Majoritatea dezvoltatorilor au început să folosească managementul de stat când Dan Abramov a lansat biblioteca Redux. Unii au început să folosească managementul de stat pentru că era o tendință, nu pentru că le lipsește. Dezvoltatorii care folosesc un proiect standard „Hello World” pentru managementul statului s-ar putea trezi rapid să scrie același cod din nou și din nou, crescând complexitatea fără niciun câștig.
În cele din urmă, unii au devenit frustrați și au abandonat complet managementul statului.
Problema mea inițială cu NgRx
Cred că această problemă generală a fost o problemă majoră cu NgRx. La început, nu am putut să vedem imaginea de ansamblu din spatele ei. NgRx este o bibliotecă, nu o paradigmă de programare sau o mentalitate. Cu toate acestea, pentru a înțelege pe deplin funcționalitatea și capacitatea de utilizare a acestei biblioteci, trebuie să ne extindem puțin mai mult cunoștințele și să ne concentrăm pe programarea funcțională. Atunci s-ar putea să ajungi să scrii cod standard și să te simți fericit pentru asta. (Vreau să spun.) Am fost cândva un sceptic NgRx; acum sunt un admirator NgRx.
Cu ceva timp în urmă, am început să folosesc managementul de stat. Am trecut prin experiența standard descrisă mai sus, așa că am decis să nu mai folosesc biblioteca. Deoarece îmi place JavaScript, încerc să obțin cel puțin cunoștințe fundamentale despre toate cadrele populare utilizate astăzi. Iată ce am învățat folosind React.
React are o caracteristică numită Hooks. La fel ca Componentele din Angular, Hook-urile sunt funcții simple care acceptă argumente și returnează valori. Un cârlig poate avea o stare în el, care se numește efect secundar. Deci, de exemplu, un buton simplu din Angular ar putea fi tradus în React astfel:
@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>; }După cum puteți vedea, aceasta este o transformare simplă:
- SimpleButtonComponent => SimpleButton
- @Input() name => props.name
- template => valoare returnată
Funcția noastră React SimpleButton are o caracteristică importantă în lumea programării funcționale: este o funcție pură . Dacă citești asta, presupun că ai auzit acel termen cel puțin o dată. NgRx.io citează funcții pure de două ori în Conceptele cheie:
- Modificările de stare sunt gestionate de funcții pure numite
reducerscare preiau starea curentă și cea mai recentă acțiune pentru a calcula o nouă stare. - Selectoarele sunt funcții pure folosite pentru a selecta, a deriva și a compune bucăți de stare.
În React, dezvoltatorii sunt încurajați să folosească Hooks ca funcții pure cât mai mult posibil. De asemenea, Angular încurajează dezvoltatorii să implementeze același model folosind paradigma Smart-Dumb Component.
Atunci mi-am dat seama că îmi lipsesc unele abilități esențiale de programare funcțională. Nu a durat mult să înțeleg NgRx, deoarece după ce am învățat conceptele cheie ale programării funcționale, am avut un „Aha! moment”: Îmi îmbunătățisem înțelegerea despre NgRx și am vrut să-l folosesc mai mult pentru a înțelege mai bine beneficiile pe care le oferă.
Acest articol împărtășește experiența mea de învățare și cunoștințele acumulate despre NgRx și programarea funcțională. Nu explic API-ul pentru NgRx sau cum să apelez acțiuni sau să folosești selectoare. În schimb, împărtășesc de ce am ajuns să apreciez că NgRx este o bibliotecă grozavă: nu este doar o tendință relativ nouă, ci oferă o serie de beneficii.
Să începem cu programarea funcțională .
Programare functionala
Programarea funcțională este o paradigmă care diferă mult de alte paradigme. Acesta este un subiect foarte complex, cu multe definiții și linii directoare. Cu toate acestea, programarea funcțională conține câteva concepte de bază și cunoașterea lor este o condiție prealabilă pentru stăpânirea NgRx (și JavaScript în general).
Aceste concepte de bază sunt:
- Funcție pură
- Stare imuabilă
- Efect secundar
Repet: este doar o paradigmă, nimic mai mult. Nu există nicio bibliotecă functional.js pe care o descărcam și să o folosim pentru a scrie software funcțional. Este doar un mod de a gândi despre scrierea aplicațiilor. Să începem cu cel mai important concept de bază: funcția pură .
Funcție pură
O funcție este considerată o funcție pură dacă urmează două reguli simple:
- Trecerea acelorași argumente returnează întotdeauna aceeași valoare
- Lipsa unui efect secundar observabil implicat în execuția funcției în interior (o schimbare externă a stării, operarea de apelare I/O etc.)
Deci o funcție pură este doar o funcție transparentă care acceptă unele argumente (sau deloc) și returnează o valoare așteptată. Sunteți sigur că apelarea acestei funcții nu va duce la efecte secundare, cum ar fi crearea de rețele sau schimbarea unei stări globale a utilizatorului.
Să aruncăm o privire la trei exemple simple:
//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); }- Prima funcție este pură deoarece va returna întotdeauna același răspuns atunci când trece aceleași argumente.
- A doua funcție nu este pură deoarece este nedeterministă și returnează răspunsuri diferite de fiecare dată când este apelată.
- A treia funcție nu este pură deoarece folosește un efect secundar (apelarea
console.log).
Este ușor să discerneți dacă funcția este pură sau nu. De ce este mai bună o funcție pură decât una impură? Pentru că este mai simplu de gândit. Imaginează-ți că citești un cod sursă și vezi un apel de funcție despre care știi că este pur. Dacă numele funcției este corect, nu trebuie să îl explorați; stii ca nu schimba nimic, intoarce ceea ce te astepti. Este esențial pentru depanare atunci când aveți o aplicație de întreprindere uriașă cu multă logică de afaceri, deoarece poate economisi timp enorm.
De asemenea, este simplu de testat. Nu trebuie să injectați nimic sau să bate joc de unele funcții în interiorul acestuia, doar treceți argumente și testați dacă rezultatul este o potrivire. Există o legătură puternică între test și logică: dacă o componentă este ușor de testat, este ușor de înțeles cum și de ce funcționează.
Funcțiile pure vin cu o funcționalitate foarte la îndemână și prietenoasă cu performanța numită memorare. Dacă știm că apelarea acelorași argumente va returna aceeași valoare, atunci putem pur și simplu să punem în cache rezultatele și să nu pierdem timpul apelând din nou. NgRx se află cu siguranță pe deasupra memorizării; acesta este unul dintre principalele motive pentru care este rapid.
Vă puteți întreba: „Dar efectele secundare? Unde merg?" În discursul său GOTO, Russ Olsen glumește că clienții noștri nu ne plătesc pentru funcții pure, ci ne plătesc pentru efecte secundare. Este adevărat: nimănui nu-i pasă de funcția Calculator pure dacă nu este tipărită undeva. Efectele secundare își au locul în universul programării funcționale. Vom vedea asta în scurt timp.
Deocamdată, să trecem la următorul pas în menținerea arhitecturilor complexe de aplicații, următorul concept de bază: stare imuabilă .
Stare imuabilă
Există o definiție simplă pentru o stare imuabilă:
- Puteți crea sau șterge doar o stare. Nu îl poți actualiza.
În termeni simpli, pentru a actualiza vârsta unui obiect User...:
let user = { username:"admin", age:28 }… ar trebui să o scrieți așa:
// Not like this newUser.age = 30; // But like this let newUser = {...user, age:29 }Fiecare modificare este un obiect nou care are proprietăți copiate din cele vechi. Ca atare, suntem deja într-o formă de stare imuabilă.
String, Boolean și Number sunt toate stări imuabile: nu puteți adăuga sau modifica valorile existente. În schimb, o dată este un obiect mutabil: manipulezi întotdeauna același obiect dată.
Imuabilitatea se aplică în întreaga aplicație: dacă treceți un obiect utilizator în interiorul funcției care își schimbă vechimea, acesta nu ar trebui să schimbe un obiect utilizator, ar trebui să creeze un nou obiect utilizator cu o vârstă actualizată și să-l returneze:
function updateAge(user, age) { return {...user, age: age) } let user = {username: 'admin', age: 29}; let newUser = updateAge(user, 32);De ce ar trebui să dedicăm timp și atenție acestui lucru? Există câteva beneficii care merită subliniate.
Un beneficiu pentru limbajele de programare back-end implică procesarea paralelă. Dacă o schimbare de stare nu depinde de o referință și fiecare actualizare este un obiect nou, puteți împărți procesul în bucăți și puteți lucra la aceeași sarcină cu nenumărate fire de execuție fără a partaja aceeași memorie. Puteți chiar să paralelizați sarcinile pe servere.
Pentru cadre precum Angular și React, procesarea paralelă este una dintre modalitățile mai benefice de îmbunătățire a performanței aplicației. De exemplu, Angular trebuie să verifice proprietățile fiecărui obiect pe care le treceți prin legăturile de intrare pentru a discerne dacă o componentă trebuie să fie redată sau nu. Dar dacă setăm ChangeDetectionStrategy.OnPush în loc de implicit, se va verifica prin referință și nu după fiecare proprietate. Într-o aplicație mare, acest lucru cu siguranță economisește timp. Dacă ne actualizăm imuabil starea, obținem acest spor de performanță gratuit.
Celălalt beneficiu pentru o stare imuabilă pe care toate limbajele de programare și cadrele îl împărtășesc este similar cu beneficiile funcțiilor pure: este mai ușor de gândit și de testat. Când o schimbare este o stare nouă născută dintr-o stare veche, știi exact la ce lucrezi și poți urmări exact cum și unde s-a schimbat starea. Nu pierdeți istoricul actualizărilor și puteți anula/reface modificările pentru stare (React DevTools este un exemplu).
Cu toate acestea, dacă o singură stare este actualizată, nu veți cunoaște istoricul acelor modificări. Gândiți-vă la o stare imuabilă, cum ar fi istoricul tranzacțiilor pentru un cont bancar. Este practic un must-have.
Acum că am analizat imuabilitatea și puritatea, să abordăm conceptul de bază rămas: efectul secundar .
Efect secundar
Putem generaliza definiția unui efect secundar:
- În informatică, se spune că o operație, o funcție sau o expresie are un efect secundar dacă modifică o valoare (valori) variabile de stare în afara mediului local. Adică are un efect observabil pe lângă returnarea unei valori (efectul principal) către invocatorul operației.
Pur și simplu, tot ceea ce schimbă o stare în afara domeniului de aplicare a funcției - toate operațiunile I/O și unele lucrări care nu sunt conectate direct la funcție - poate fi considerat un efect secundar. Cu toate acestea, trebuie să evităm utilizarea efectelor secundare în cadrul funcțiilor pure, deoarece efectele secundare contrazic filozofia de programare funcțională. Dacă utilizați o operație I/O în interiorul unei funcții pure, atunci aceasta încetează să mai fie o funcție pură.
Cu toate acestea, trebuie să avem efecte secundare undeva, deoarece o aplicare fără ele ar fi inutilă. În Angular, nu numai că funcțiile pure trebuie protejate împotriva efectelor secundare, ci trebuie să evităm să le folosim și în Componente și Directive.
Să examinăm cum putem implementa frumusețea acestei tehnici în cadrul Angular.
Programare unghiulară funcțională
Unul dintre primele lucruri pe care trebuie să le înțelegeți despre Angular este necesitatea de a decupla componentele în componente mai mici cât mai des posibil pentru a facilita întreținerea și testarea. Acest lucru este necesar, deoarece trebuie să ne împărțim logica de afaceri. De asemenea, dezvoltatorii Angular sunt încurajați să lase componentele doar în scopuri de randare și să mute toată logica de afaceri în interiorul serviciilor.
Pentru a extinde aceste concepte, utilizatorii Angular au adăugat modelul „Dumb-Smart Component” la vocabularul lor. Acest model necesită ca apelurile de service să nu existe în interiorul componentelor mici. Deoarece logica de afaceri rezidă în servicii, trebuie să apelăm în continuare aceste metode de service, să așteptăm răspunsul lor și abia apoi să facem orice modificări de stare. Deci, componentele au o anumită logică comportamentală în interiorul lor.
Pentru a evita acest lucru, putem crea o componentă inteligentă (componentă rădăcină), care conține logica de afaceri și comportament, transmitem stările prin Proprietăți de intrare și apelăm acțiuni ascultând parametrii de ieșire. În acest fel, componentele mici sunt cu adevărat doar în scopuri de randare. Desigur, componenta noastră rădăcină trebuie să aibă în interior niște apeluri de serviciu și nu le putem elimina pur și simplu, ci utilitatea sa ar fi limitată doar la logica de afaceri, nu la randare.
Să ne uităm la un exemplu de Counter Component. Un contor este o componentă care are două butoane care măresc sau scad valoarea și un displayField care afișează currentValue . Așadar, ajungem cu patru componente:
- Counter Container
- Buton de creștere
- DecreaseButton
- Valoarea curentă
Toată logica trăiește în interiorul CounterContainer , așa că toate trei sunt doar randeri. Iată codul pentru cei trei:
@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(); }Uite cât de simple și pure sunt. Nu au nicio stare sau efecte secundare, depind doar de proprietățile de intrare și de evenimentele emitente. Imaginează-ți cât de ușor este să le testezi. Le putem numi componente pure, deoarece asta sunt cu adevărat. Acestea depind doar de parametrii de intrare, nu au efecte secundare și returnează întotdeauna aceeași valoare (șir de șablon) prin transmiterea acelorași parametri.

Deci, funcțiile pure din programarea funcțională sunt transferate în componentele pure din Angular. Dar unde se duce toată logica? Logica este încă acolo, dar într-un loc puțin diferit, și anume 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; } } După cum puteți vedea, logica comportamentului trăiește în CounterContainer , dar partea de randare lipsește (declară componente în interiorul șablonului), deoarece partea de randare este pentru componente pure.
Am putea injecta cât de mult serviciu dorim, deoarece ne ocupăm de toate manipulările de date și schimbările de stare aici. Un lucru care merită menționat este că, dacă avem o componentă imbricată adânc, nu trebuie să creăm doar o componentă la nivel de rădăcină. Îl putem împărți în componente inteligente mai mici și putem folosi același model. În cele din urmă, depinde de complexitatea și nivelul imbricat pentru fiecare componentă.
Putem sări cu ușurință din acel model în biblioteca NgRx în sine, care este doar un strat deasupra acestuia.
Biblioteca NgRx
Putem împărți orice aplicație web în trei părți principale:
- Logica de afaceri
- Starea aplicației
- Logica de redare
Logica de afaceri este tot comportamentul care se întâmplă cu aplicația, cum ar fi rețea, intrare, ieșire, API etc.
Starea aplicației este starea aplicației. Poate fi global, ca utilizator autorizat în prezent, și, de asemenea, local, ca valoare curentă a componentei contor.
Logica de randare cuprinde randarea, cum ar fi afișarea datelor folosind DOM, crearea sau eliminarea elementelor și așa mai departe.
Folosind modelul Dumb-Smart, am decuplat logica de redare de logica de afaceri și starea aplicației, dar le putem împărți și pentru că ambele sunt diferite conceptual unul de celălalt. Starea aplicației este ca un instantaneu al aplicației dvs. în timpul curent. Logica de afaceri este ca o funcționalitate statică care este întotdeauna prezentă în aplicația dvs. Cel mai important motiv pentru a le împărți este că Business Logic este în mare parte un efect secundar pe care vrem să-l evităm în codul aplicației cât mai mult posibil. Acesta este momentul în care biblioteca NgRx, cu paradigma sa funcțională, strălucește.
Cu NgRx decuplați toate aceste părți. Există trei părți principale:
- Reductoare
- Acțiuni
- Selectoare
Combinate cu programarea funcțională, toate trei se combină pentru a ne oferi un instrument puternic pentru a gestiona aplicații de orice dimensiune. Să examinăm fiecare dintre ele.
Reductoare
Un reductor este o funcție pură, care are o semnătură simplă. Ia o stare veche ca parametru și returnează o stare nouă, fie derivată din cea veche, fie una nouă. Starea în sine este un singur obiect, care trăiește cu ciclul de viață al aplicației dvs. Este ca o etichetă HTML, un singur obiect rădăcină.
Nu puteți modifica direct un obiect de stare, trebuie să îl modificați cu reductoare. Aceasta are o serie de beneficii:
- Logica de schimbare a stării trăiește într-un singur loc și știi unde și cum se schimbă starea.
- Funcțiile reductorului sunt funcții pure, care sunt ușor de testat și gestionat.
- Deoarece reductoarele sunt funcții pure, acestea pot fi memorate, făcând posibilă stocarea lor în cache și evitarea calculelor suplimentare.
- Schimbările de stat sunt imuabile. Nu actualizați niciodată aceeași instanță. În schimb, returnezi întotdeauna unul nou. Acest lucru permite o experiență de depanare „călătorie în timp”.
Acesta este un exemplu banal de reductor:
function usernameReducer(oldState, username) { return {...oldState, username} }Chiar dacă este un reductor manechin foarte simplu, este scheletul tuturor reductoarelor lungi și complexe. Toate au aceleași beneficii. Am putea avea sute de reductoare în aplicația noastră și putem face câte vrem.
Pentru Componenta noastră de contor, starea și reductoarele noastre ar putea arăta astfel:
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 }Am eliminat starea din componentă. Acum avem nevoie de o modalitate de a ne actualiza starea și de a apela reductorul corespunzător. Atunci intră în joc acțiunile.
Acțiuni
O acțiune este o modalitate de a notifica NgRx să apeleze un reducător și să actualizeze starea. Fără asta, ar fi lipsit de sens să folosești NgRx. O acțiune este un obiect simplu pe care îl atașăm la reductorul de curent. După apelarea acestuia, va fi apelat reductorul corespunzător, așa că în exemplul nostru am putea avea următoarele acțiuni:
enum CounterActions { IncreaseValue = '[Counter Component] Increase Value', DecreaseValue = '[Counter Component] Decrease Value', } on(CounterActions.IncreaseValue,increaseReducer); on(CounterActions.DecreaseValue,decreaseReducer);Acțiunile noastre sunt atașate reductoarelor. Acum putem modifica în continuare Componenta Container și să apelăm acțiuni adecvate atunci când este necesar:
@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); } }Notă: am eliminat starea și vom adăuga înapoi în curând .
Acum CounterContainer -ul nostru nu are nicio logică de schimbare a stării. Știe doar ce să trimită. Acum avem nevoie de o modalitate de a afișa aceste date în vizualizare. Asta e utilitatea selectoarelor.
Selectoare
Un selector este, de asemenea, o funcție pură foarte simplă, dar, spre deosebire de un reductor, nu actualizează starea. După cum sugerează și numele, selectorul doar îl selectează. În exemplul nostru, am putea avea trei selectoare simple:
function selectCurrentValue(state) { return state.currentValue; } function selectDicreaseIsDisabled(state) { return state.decreaseDisabled; } function selectIncreaseIsDisabled(state) { return state.increaseDisabled; } Folosind acești selectori, am putea selecta fiecare parte dintr-o stare din componenta noastră inteligentă CounterContainer .
@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); } }Aceste selecții sunt asincrone în mod implicit (la fel ca și Observabilele în general). Acest lucru nu are nicio importanță, cel puțin din punct de vedere al modelului. Același lucru ar fi valabil și pentru unul sincron, deoarece am selecta doar ceva din starea noastră.
Să facem un pas înapoi și să ne uităm la imaginea de ansamblu pentru a vedea ce am realizat până acum. Avem o aplicație Counter, care are trei părți principale care sunt aproape decuplate una de cealaltă. Nimeni nu știe cum se gestionează starea aplicației sau cum redă starea stratul de randare.
Părțile decuplate folosesc puntea (Acțiuni, Selectoare) pentru a se conecta între ele. Ele sunt decuplate în așa măsură încât am putea lua întregul cod al aplicației de stat și să-l mutăm într-un alt proiect, ca pentru o versiune mobilă, de exemplu. Singurul lucru pe care ar trebui să îl implementăm ar fi redarea. Dar ce zici de testare?
În opinia mea umilă, testarea este cea mai bună parte a NgRx. Testarea acestui exemplu de proiect este asemănătoare cu a juca tic-tac-toe. Există doar funcții pure și componente pure, așa că testarea lor este o ușoară. Acum imaginați-vă dacă acest proiect devine mai mare, cu sute de componente. Dacă urmam același model, am adăuga din ce în ce mai multe piese împreună. Nu ar deveni un bloc de cod sursă dezordonat, imposibil de citit.
Aproape am terminat. Mai rămâne un singur lucru important de acoperit: efectele secundare. Am menționat de multe ori efectele secundare până acum, dar m-am oprit să explic unde să le depozitez.
Asta pentru că efectele secundare sunt cireașa de pe tort și prin construirea acestui model, este foarte ușor să le eliminați din codul aplicației.
Efecte secundare
Să presupunem că aplicația noastră de contor are un temporizator în ea și la fiecare trei secunde crește automat valoarea cu una. Acesta este un simplu efect secundar, care trebuie să trăiască undeva. Este același efect secundar, prin definiție, ca și o solicitare Ajax.
Dacă ne gândim la efectele secundare, majoritatea au două motive principale de a exista:
- Făcând orice în afara mediului de stat
- Se actualizează starea aplicației
De exemplu, stocarea unei stări în interiorul LocalStorage este prima opțiune, în timp ce actualizarea stării din răspunsul Ajax este a doua. Dar ambele au aceeași semnătură: fiecare efect secundar trebuie să aibă un punct de plecare. Trebuie apelat cel puțin o dată pentru a-l solicita să înceapă acțiunea.
După cum am subliniat mai devreme, NgRx are un instrument frumos pentru a da cuiva o comandă. Asta este o acțiune. Am putea numi orice efect secundar prin trimiterea unei acțiuni. Pseudocodul ar putea arăta astfel:
function startTimer(){ setInterval(()=>{ console.log("Hello application"); },3000) } on(CounterActions.StartTime,startTimer) ... // We start timer by dispatching an action dispatch(CounterActions.StartTime);Este destul de banal. După cum am menționat mai devreme, efectele secundare fie actualizează ceva, fie nu. Dacă un efect secundar nu actualizează nimic, nu este nimic de făcut; doar o lăsăm. Dar dacă vrem să actualizăm o stare, cum o facem? Același mod în care o componentă încearcă să actualizeze o stare: apelând o altă acțiune. Deci numim o acțiune în interiorul efectului secundar, care actualizează starea:
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);Acum avem o aplicație complet funcțională.
Rezumând experiența noastră NgRx
Există câteva subiecte importante pe care aș dori să le menționez înainte de a termina călătoria noastră NgRx:
- Codul afișat este un pseudo-cod simplu pe care l-am inventat pentru articol; este potrivit doar pentru scopuri demonstrative. NgRx este locul în care trăiesc surse reale.
- Nu există niciun ghid oficial care să dovedească teoria mea despre conectarea programării funcționale cu biblioteca NgRx. Este doar părerea mea formată după ce am citit zeci de articole și mostre de cod sursă create de oameni cu înaltă calificare.
- După ce utilizați NgRx, veți realiza cu siguranță că este mult mai complex decât acest exemplu simplu. Scopul meu nu a fost să arăt mai simplu decât este de fapt, ci să vă arăt că, deși este puțin complex și poate duce chiar la o cale mai lungă către destinație, merită efortul suplimentar.
- Cea mai proastă utilizare pentru NgRx este folosirea acestuia peste tot, indiferent de dimensiunea sau complexitatea aplicației. Există unele cazuri în care nu ar trebui să utilizați NgRx; de exemplu, în forme. Este aproape imposibil să implementezi formulare în interiorul NgRx. Formularele sunt lipite de DOM în sine; ei nu pot trăi separat. Dacă încercați să le decuplați, vă veți descoperi că urăști nu numai NgRx, ci și tehnologia web în general.
- Uneori folosirea aceluiași cod boilerplate, chiar și pentru un mic exemplu, se poate transforma într-un coșmar, chiar dacă ne poate aduce beneficii pe viitor. Dacă acesta este cazul, doar integrați-vă cu o altă bibliotecă uimitoare, care face parte din ecosistemul NgRx (ComponentStore).
