Fundamentale elementer i NgRx: Store, Actions, Reducers, Selectors, Effects

Lageret er nøgleelementet i hele tilstandsstyringsprocessen. Den indeholder tilstanden og letter interaktionen mellem komponenterne og tilstanden. Du kan få en reference til lageret via Angular dependency injection, som vist nedenfor.

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

Denne butiksreference kan efterfølgende bruges til to primære operationer:

  • Til at sende handlinger til lageret via store.dispatch(…)-metoden, som igen vil udløse reduceringer og effekter
  • Til at hente programtilstanden via selektorer

Struktur for et træ af tilstandsobjekter

Sæt, at dit program består af to funktionsmoduler kaldet User og Product. Hvert af disse moduler håndterer forskellige dele af den overordnede tilstand. Produktoplysninger vil altid blive vedligeholdt i products-afsnittet i tilstanden. Brugeroplysninger vil altid blive vedligeholdt i user-afsnittet i tilstanden. Disse afsnit kaldes også for skiver.

Aktioner

En aktion er en instruktion, som du sender til lageret, eventuelt med nogle metadata (payload). På grundlag af handlingstypen beslutter lageret, hvilke operationer der skal udføres. I kode repræsenteres en handling af et almindeligt gammelt JavaScript-objekt med to hovedattributter, nemlig type og payload. payload er en valgfri attribut, der vil blive brugt af reducerne til at ændre tilstanden. Følgende kodestump og figur illustrerer dette koncept:

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

NgRx version 8 indeholder en hjælpefunktion kaldet createAction til at definere action creators (ikke handlinger, men action creators). Følgende er en eksempelkode til dette:

Du kan derefter bruge login action creator (som er en funktion) til at opbygge handlinger og sende dem til lageret som vist nedenfor. user er det payload objekt, som du sender ind i handlingen.

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

Reducers

Reducers er ansvarlige for at ændre tilstanden og returnere et nyt tilstandsobjekt med ændringerne. Reducers tager imod to parametre, den aktuelle tilstand og handlingen. På grundlag af den modtagne handlingstype udfører reducers visse ændringer af den aktuelle tilstand og producerer en ny tilstand. Dette koncept er vist i diagrammet nedenfor.

I lighed med handlinger tilbyder NgRx en hjælpefunktion kaldet createReducer til at oprette reducers. Et typisk createReducer-funktionskald ville se ud som følgende:

Som du kan se, tager den den indledende tilstand (tilstanden ved programmets opstart) og en-til-mange-tilstandsændringsfunktioner, der definerer, hvordan der skal reageres på forskellige handlinger. Hver af disse tilstandsændringsfunktioner modtager den aktuelle tilstand og handlingen som parametre og returnerer en ny tilstand.

Effekter

Effekter giver dig mulighed for at udføre sideeffekter, når en handling sendes til lageret. Lad os prøve at forstå dette gennem et eksempel. Når en bruger logger ind på et program, sendes en handling med type Login Action til butikken med brugeroplysningerne i payload. En reduktionsfunktion vil lytte til denne handling og ændre tilstanden med brugeroplysningerne. Som en sideeffekt ønsker du desuden også at gemme brugeroplysningerne i browserens lokale lager. En effekt kan bruges til at udføre denne ekstra opgave (sideeffekt).

Der er flere måder at oprette effekter på i NgRx. Følgende er en rå og selvforklarende måde at oprette effekter på. Bemærk, at du normalt ikke bruger denne metode til at oprette effekter. Jeg tog kun dette som et eksempel for at forklare, hvad der sker bag gardinet.

  • actions$ observable vil udsende handlinger, der modtages af butikken. Disse værdier vil gå gennem en operatørkæde.
  • ofType er den første operatør, der anvendes. Dette er en særlig operatør, der leveres af NgRx (Not RxJS) til at filtrere handlinger ud på baggrund af deres type. I dette tilfælde vil kun handlinger af typen login få lov til at gå gennem resten af operatørkæden.
  • tap er den anden operatør, der bruges i kæden til at gemme brugeroplysningerne i browserens lokale lager. tap-operatoren bruges generelt til at udføre sideeffekter i en operatørkæde. Endeligt skal vi manuelt abonnere på login$-observablen.

Denne fremgangsmåde har dog et par store ulemper.

  • Du skal manuelt abonnere på observablen, hvilket ikke er en god praksis. På denne måde skal du altid manuelt afmelde dig manuelt, hvilket fører til manglende vedligeholdbarhed.
  • Hvis der opstår en fejl i operatørkæden, vil observablen fejle og holde op med at udsende efterfølgende værdier (handlinger). Som følge heraf vil sideeffekten ikke blive udført. Derfor skal du have en mekanisme på plads til manuelt at oprette en ny observabelinstans og genindmelde dig, hvis der opstår en fejl.

For at overvinde disse problemer indeholder NgRx en hjælpefunktion kaldet createEffect til at oprette effekter. Et typisk createEffect-funktionskald ville se ud som følger:

createEffect-metoden tager en funktion ind, der returnerer en observabel og (eventuelt) et konfigurationsobjekt som parametre.

NgRx håndterer abonnementet på den observabel, der returneres af hjælpefunktionen, og derfor behøver du ikke at abonnere eller afmelde dig manuelt. Hvis der opstår en fejl i operatørkæden, opretter NgRx desuden en ny observabel og foretager en ny tilmelding for at sikre, at sideeffekten altid bliver udført.

Hvis dispatch er true (standardværdi) i konfigurationsobjektet, returnerer createEffect-metoden en Observable<Action>. Ellers returnerer den en Observable<Unknown>. Hvis dispatch-egenskaben er true, abonnerer NgRx på den returnerede observable af type Observable<Action> og sender de modtagne handlinger til lageret.

Hvis du ikke mapper den modtagne handling til en anden handlingstype i operatørkæden, skal du indstille dispatch til false. Ellers vil udførelsen resultere i en uendelig løkke, da den samme handling vil blive afsendt og modtaget i actions$-strømmen igen og igen. Du behøver f.eks. ikke at indstille dispatch til false i nedenstående kode, fordi du mapper den oprindelige handling til en anden type handling i operatørkæden.

I ovenstående scenarie,

  • Effekt modtager handlinger af type loadAllCourses.
  • En API påkaldes, og kurser indlæses som sideeffekt.
  • API-svaret til en handling af type allCoursesLoaded tilknyttes, og de indlæste kurser overføres som payload til handlingen.
  • Og endelig sendes denallCoursesLoaded-handling, der er oprettet, til butikken. Dette gøres af NgRx under motorhjelmen.
  • En reducer lytter til den indkommende allCoursesLoaded-aktion og ændrer tilstanden med de indlæste kurser.

Selectors

Selectors er rene funktioner, der bruges til at få udsnit af butikkens tilstand. Som vist nedenfor kan du forespørge tilstanden, selv uden at bruge selektorer. Men denne fremgangsmåde er igen forbundet med et par store ulemper.

const isLoggedIn$ = this.store.pipe(map(state => !!state.user));
  • store er en observable, som du kan abonnere på. Hver gang butikken modtager en handling, vil store skubbe tilstandsobjektet videre til sine abonnenter.
  • Du kan bruge mapping-funktioner til at få udsnit af tilstanden og udføre eventuelle beregninger, hvis det er nødvendigt. I ovenstående eksempel indhenter vi user-slice i tilstandsobjekttræet og konverterer den til en boolean for at bestemme, om brugeren er logget ind eller ej. Du kan enten manuelt abonnere på isLoggedIn$-observablen eller bruge den i en Angular-skabelon med async pipe til at læse de værdier, der udsendes.

Denne tilgang har dog en stor ulempe. Generelt modtager butikken ofte handlinger fra forskellige dele af applikationen. I henhold til ovenstående implementering vil der, hver gang butikken modtager en handling, blive udsendt et tilstandsobjekt af butikken. Og dette tilstandsobjekt vil igen gå gennem kortlægningsfunktionen og opdatere brugergrænsefladen.

Hvis resultatet af kortlægningsfunktionen ikke har ændret sig i forhold til sidste gang, er der imidlertid ikke behov for at opdatere brugergrænsefladen igen. Hvis resultatet af map(state => !!state.user) f.eks. ikke er ændret i forhold til sidste udførelse, behøver vi ikke atter at skubbe resultatet videre til UI/Subscriber. For at opnå dette har NgRx (ikke RxJS) indført en særlig operatør kaldet select. Med select-operatoren ændres ovenstående kode som følger:

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

select-operatoren forhindrer, at værdier skubbes til UI/subscribers, hvis resultatet af mappingfunktionen ikke er ændret i forhold til sidste gang.

Denne fremgangsmåde kan forbedres yderligere. Selv om select-operatoren ikke skubber uændrede værdier til UI/Subscribers, skal den stadig tage tilstandsobjektet og foretage beregningen for at udlede resultatet hver gang.

Som allerede forklaret ovenfor vil en state blive udsendt af observablen, når lageret modtager en handling fra applikationen. En handling opdaterer ikke altid tilstanden. Hvis tilstanden ikke er ændret, ændres resultatet af beregningen af mappingfunktionen heller ikke. Derfor behøver vi ikke at foretage beregningen igen, hvis det udsendte state-objekt ikke har ændret sig i forhold til sidste gang. Det er her, at selektorerne kommer ind i billedet.

En selektor er en ren funktion, der vedligeholder en hukommelse om de tidligere udførelser. Så længe input ikke er ændret, vil output ikke blive genberegnet. I stedet returneres output fra hukommelsen. Denne proces kaldes memoization.

NgRx indeholder en hjælpefunktion kaldet createSelector til at opbygge selektorer med memoization-funktion. Nedenstående er et eksempel på hjælpefunktionen createSelector.

Funktionen createSelector tager imod en-til-mange-mappingsfunktioner, der giver forskellige skiver af tilstanden, og en projektorfunktion, der udfører beregningen. Projektorfunktionen vil ikke blive påkaldt, hvis tilstandsskiverne ikke er ændret i forhold til den seneste udførelse. For at kunne bruge den oprettede selektorfunktion skal den overføres som et argument til select-operatoren.

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

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.