Grundläggande element i NgRx: Store, Actions, Reducers, Selectors, Effects
Store är nyckelelementet i hela tillståndshanteringsprocessen. Den innehåller tillståndet och underlättar interaktionen mellan komponenterna och tillståndet. Du kan få en referens till lagret via Angular dependency injection, enligt nedan.
constructor(private store: Store<AppState>) {}
Denna store-referens kan därefter användas för två primära operationer:
- För att skicka åtgärder till lagret via
store.dispatch(…)
-metoden, som i sin tur utlöser reducerare och effekter - För att hämta programtillståndet via selektorer
Struktur för ett träd med tillståndsobjekt
Förutsatt att programmet består av två funktionsmoduler som heter User och Product. Var och en av dessa moduler hanterar olika delar av det övergripande tillståndet. Produktinformation kommer alltid att upprätthållas i avsnittet products
i tillståndet. Användarinformation kommer alltid att underhållas i avsnittet user
i tillståndet. Dessa sektioner kallas också för skivor.
Aktioner
En åtgärd är en instruktion som du skickar till lagret, eventuellt med vissa metadata (payload). Baserat på åtgärdstypen bestämmer lagret vilka operationer som ska utföras. I koden representeras en åtgärd av ett vanligt JavaScript-objekt med två huvudattribut, nämligen type
och payload
. payload
är ett valfritt attribut som kommer att användas av reducerare för att ändra tillståndet. Följande kodutdrag och figur illustrerar detta koncept:
{ "type": "Login Action", "payload": { userProfile: user }}
NgRx version 8 tillhandahåller en hjälpfunktion som kallas createAction
för att definiera åtgärdsskapare (inte åtgärder, utan åtgärdsskapare). Följande är en exempelkod för detta:
Du kan sedan använda login
action creator (som är en funktion) för att bygga åtgärder och skicka dem till lagret som visas nedan. user
är payload
-objektet som du skickar in i åtgärden.
this.store.dispatch(login({user}));
Reducers
Reducers ansvarar för att ändra tillståndet och returnera ett nytt tillståndsobjekt med ändringarna. Reducers tar in två parametrar, det aktuella tillståndet och åtgärden. Baserat på den mottagna åtgärdstypen kommer reducers att utföra vissa ändringar av det aktuella tillståndet och producera ett nytt tillstånd. Detta koncept visas i diagrammet nedan.
I likhet med åtgärder tillhandahåller NgRx en hjälpfunktion som heter createReducer
för att skapa reducers. Ett typiskt createReducer
-funktionsanrop skulle se ut på följande sätt:
Som du kan se tar den in det initiala tillståndet (tillståndet vid programstart) och en till flera funktioner för tillståndsändringar som definierar hur man ska reagera på olika åtgärder. Var och en av dessa tillståndsändringsfunktioner tar emot det aktuella tillståndet och åtgärden som parametrar och returnerar ett nytt tillstånd.
Effekter
Effekter gör det möjligt att utföra sidoeffekter när en åtgärd skickas till lagret. Låt oss försöka förstå detta genom ett exempel. När en användare loggar in i ett program skickas en åtgärd med type
Login Action
till butiken med användarinformationen i payload
. En reducerfunktion lyssnar på denna åtgärd och ändrar tillståndet med användarinformationen. Som en sidoeffekt vill du dessutom också spara användarinformation i webbläsarens lokala lagring. En effekt kan användas för att utföra denna ytterligare uppgift (sidoeffekt).
Det finns flera sätt att skapa effekter i NgRx. Följande är ett rått och självförklarande sätt att skapa effekter. Observera att du i allmänhet inte använder den här metoden för att skapa effekter. Jag tog bara detta som ett exempel för att förklara vad som händer bakom ridån.
-
actions$
observable kommer att sända ut åtgärder som tas emot av butiken. Dessa värden kommer att gå genom en operatörskedja. -
ofType
är den första operatören som används. Detta är en särskild operatör som tillhandahålls av NgRx (Not RxJS) för att filtrera ut åtgärder baserat på deras typ. I det här fallet kommer endast åtgärder av typenlogin
att tillåtas gå igenom resten av operatörskedjan.
tap
är den andra operatören som används i kedjan för att lagra användarinformation i webbläsarens lokala lagring. Operatören tap
används i allmänhet för att utföra sidoeffekter i en operatörskedja. Slutligt måste vi manuellt prenumerera på observabeln login$
.
Det här tillvägagångssättet har dock ett par stora nackdelar.
- Du måste manuellt prenumerera på observabeln, vilket inte är någon bra metod. På så sätt måste du alltid avregistrera dig manuellt, vilket leder till bristande underhållbarhet.
- Om ett fel dyker upp i operatörskedjan kommer observabeln att göra fel och sluta avge efterföljande värden (åtgärder). Som ett resultat av detta kommer sidoeffekten inte att utföras. Därför måste du ha en mekanism på plats för att manuellt skapa en ny observable-instans och återigen prenumerera om ett fel inträffar.
För att övervinna dessa problem tillhandahåller NgRx en hjälpfunktion som heter createEffect
för att skapa effekter. Ett typiskt createEffect
-funktionsanrop skulle se ut på följande sätt:
Metoden createEffect
tar in en funktion som returnerar en observabel och (valfritt) ett konfigurationsobjekt som parametrar.
NgRx hanterar prenumerationen på observabeln som returneras av stödfunktionen, och därför behöver du inte manuellt prenumerera eller avsluta prenumerationen. Om något fel uppstår i operatörskedjan kommer NgRx dessutom att skapa en ny observabel och prenumerera på nytt för att se till att sidoeffekten alltid utförs.
Om dispatch
är true
(standardvärde) i konfigurationsobjektet returnerar createEffect
-metoden en Observable<Action>
. I annat fall returnerar den ett Observable<Unknown>
. Om egenskapen dispatch
är true
prenumererar NgRx på den returnerade observabeln type
Observable<Action>
och skickar de mottagna åtgärderna till lagret.
Om du inte mappar den mottagna åtgärden till en annan typ av åtgärd i operatörskedjan måste du ställa in dispatch
till false
. Annars kommer exekveringen att resultera i en oändlig slinga, eftersom samma åtgärd kommer att skickas och tas emot i actions$
strömmen om och om igen. Du behöver till exempel inte ställa in dispatch
till false
i koden nedan eftersom du mappar den ursprungliga åtgärden till en annan typ av åtgärd i operatörskedjan.
I ovanstående scenario,
- Effect tar emot åtgärder av
type
loadAllCourses
. - Ett API åberopas och kurser laddas som sidoeffekt.
- API-svaret till en åtgärd av
type
allCoursesLoaded
mappas och de laddade kurserna överförs sompayload
till åtgärden. - Och slutligen skickas
allCoursesLoaded
-åtgärden som har skapats till butiken. Detta görs av NgRx under huven. - En reducer lyssnar på den inkommande
allCoursesLoaded
-åtgärden och ändrar tillståndet med de inlästa kurserna.
Selectors
Selectors är rena funktioner som används för att erhålla delar av tillståndet i butiken. Som visas nedan kan du fråga efter tillståndet även utan att använda selektorer. Men det här tillvägagångssättet har återigen ett par stora nackdelar.
const isLoggedIn$ = this.store.pipe(map(state => !!state.user));
-
store
är en observabel som du kan prenumerera på. Varje gång butiken tar emot en åtgärd kommerstore
att skicka state-objektet vidare till sina prenumeranter. - Du kan använda mappningsfunktioner för att få delmängder av state och utföra eventuella beräkningar om det behövs. I exemplet ovan hämtar vi
user
-skivan i tillståndsobjektträdet och omvandlar den till en boolean för att avgöra om användaren har loggat in eller inte.
Du kan antingen manuellt prenumerera på isLoggedIn$
-observabeln eller använda den i en Angular-mall med async-pipe för att läsa de värden som sänds ut.
Det här tillvägagångssättet har dock en stor nackdel. I allmänhet får butiken ofta åtgärder från olika delar av applikationen. Enligt ovanstående implementering kommer ett statusobjekt att sändas ut av butiken varje gång butiken tar emot en åtgärd. Och detta tillståndsobjekt kommer återigen att gå igenom mappningsfunktionen och uppdatera användargränssnittet.
Om resultatet av mappningsfunktionen inte har ändrats från förra gången finns det dock inget behov av att uppdatera användargränssnittet igen. Om till exempel resultatet av map(state => !!state.user)
inte har ändrats från förra utförandet behöver vi inte återigen skjuta resultatet vidare till UI/Subscriber. För att uppnå detta har NgRx (inte RxJS) infört en särskild operatör som heter select
. Med operatören select
kommer ovanstående kod att ändras på följande sätt:
const isLoggedIn$ = this.store.pipe(select(state => !!state.user));
Operatorn select
förhindrar att värden skjuts upp till UI/subscribers om resultatet av mappningsfunktionen inte har ändrats från förra gången.
Detta tillvägagångssätt kan förbättras ytterligare. Även om select
-operatören inte skickar oförändrade värden till UI/Subscribers måste den fortfarande ta state-objektet och göra beräkningen för att härleda resultatet varje gång.
Som redan förklarats ovan kommer en state
att sändas ut av observabeln när lagret tar emot en åtgärd från programmet. En åtgärd uppdaterar inte alltid tillståndet. Om tillståndet inte har ändrats kommer resultatet av beräkningen av mappningsfunktionen inte heller att ändras. Därför behöver vi inte göra beräkningen igen om det emitterade state
-objektet inte har ändrats från förra gången. Det är här som selektorerna kommer in i bilden.
En selektor är en ren funktion som upprätthåller ett minne av tidigare utföranden. Så länge inmatningen inte har ändrats kommer utmatningen inte att räknas om. Istället returneras utgången från minnet. Denna process kallas memotisering.
NgRx tillhandahåller en hjälpfunktion som heter createSelector
för att bygga selektorer med memotiseringsförmåga. Nedan följer ett exempel på hjälpfunktionencreateSelector
.
Funktionen createSelector
tar in en till flera mappningsfunktioner som ger olika delar av tillståndet och en projektorfunktion som utför beräkningen. Projektorfunktionen kommer inte att anropas om tillståndsskivorna inte har ändrats från den senaste utförandet. För att kunna använda den skapade selektorfunktionen måste du skicka den som ett argument till select
-operatören.
this.isLoggedIn$ = this.store .pipe( select(isLoggedIn) );