Angular State Management met ngRx
Auteur: Danny Holstein
Tijdens het ontwikkelen van Angular applicaties is het in bepaalde situaties nodig dat de state wordt bijgehouden van bijvoorbeeld de paginering, het aantal items per pagina en input-velden.
Een uitweg is om de code voor state management zelf te schrijven. Maar het gevaar hiervan is dat er veel herhalende code ontstaat. State management is vooral van belang in grote en complexe applicaties waar de verschillende componenten data delen.
Aanvankelijk leek ngRx een complexe oplossing. Maar na het doornemen van tutorials en het toepassen van code bleek dit een goede oplossing te bieden.
Wat is ngRx?
ngRx is een library voor state management die buiten Angular omwerkt. Deze library is een wrapper rond Redux (de afkorting ngRx = ng voor Angular + Rx voor Redux). Binnen Angular bestond hier behoefte aan omdat er veel services zijn waar vaak veel functionaliteit in zit en data wordt gedeeld.
De grote hoeveelheid aan services kan een probleem worden in een team van developers waar iedereen zijn eigen state-management-code gaat schrijven. Om dit laatste te voorkomen, wordt er met ngRx code geschreven die er voor het hele project hetzelfde uitziet. Hierdoor begrijpt iedere developer wat er is gedaan en is ook de code makkelijker aan te passen.
ngRx dient verder om de globale en lokale state van een applicatie te beheren. De code is schoner doordat de verschillende delen (zoals Actions, Reducers en Effects) van elkaar geïsoleerd zijn. Daarnaast is er een goede tool die developers helpt met het ontwikkelen en debuggen van de states.
Zie de afbeelding hieronder voor een schematische weergave van ngRx:
De onderdelen van ngRx
De store is hierbij de door RxJS (dat is een andere library: Reactive Extensions for JavaScript) aangedreven state-management voor Angular applicaties.
Een effect is zijeffect van de applicatie, bijvoorbeeld: het ophalen van data uit een API, het loggen van gegevens in het console of het opslaan van gegevens in localStorage of sessionStorage.
Actions daarentegen beschrijven unieke gebeurtenissen die verzonden worden vanuit componenten en services. Dit kan het ophalen van data zijn, het toevoegen of verwijderen van items, wat er moet gebeuren om een fout af te handelen, etc.
Een reducer is een functie die de applicatie-state verandert. Dit zijn pure functions die de huidige state en de recentste action meenemen om de nieuwe state te berekenen. De nieuwe state wordt uiteindelijk in het component weergegeven.
ngRx gebruiken in een Angular applicatie
De ngRx library is niet standaard opgenomen in Angular. Om ngRx te gebruiken moet dit eerste geïnstalleerd worden – in het bijzonder de 3 pakketten: @ngrx/store, @ngrx/effects en @ngrx/store-devtools. De versie van ngRx moet overeenkomen met de versie van Angular.
Als demonstratie worden er gebruikers/Users opgehaald en als state opgeslagen. In app.config.ts wordt providerStore(), provideState() en provideEffects() toegevoegd. Zie de afbeelding hieronder:
De actions – in user.actions.ts – beschrijven de gebeurtenissen (events) die kunnen plaatsvinden. Actions worden gemaakt met de createAction() functie. In de voorbeeldcode hieronder zijn er 3 actions: getUsers (ophalen van de gebruikers), getUsersSuccess (de data is succesvol opgehaald) en getUsersFailure (het ophalen van de data is mislukt). Een simpele UserInterface-array wordt gebruikt om de gebruikers in op te slaan. Met props kan data worden doorgegeven als object. Zie de afbeelding hieronder:
In de reducer zijn de functies opgenomen die de state verandert. Hierin komen de 3 bovenstaande actions voor. Maar eerst wordt er een ‘eerste staat’ (initial state) gedefinieerd waar nog geen data is geladen. Reducers worden gemaakt met de createReducer() functie. Daarbinnen is de initial state en verschillende on() methoden opgenomen.
In de on() methode wordt de state veranderd. Binnen reducers kan de state echter niet direct veranderd worden. Daarom wordt er een nieuw object gemaakt door middel van de spread operator, zie: { ...state }. En hiermee wordt de isLoading-property overschreven met de waarde true. Zie de afbeelding hieronder:
Wanneer de hier bovenstaande on() met de action UserActions.getUsers wordt overgehaald, dan worden de gebruikers vanuit de API opgehaald. Met ngRx wordt de data opgehaald met een effect voor asynchrone processen. Een effect zal wachten tot er een specifieke action wordt verzonden. Wanneer het ophalen van de data succesvol is, wordt er de action UserActions.getUserSuccess verstuurd (zie afbeelding hierboven) – maar wanneer het ophalen van de data mislukt, dan wordt de action UserActions.getUsersFailure verstuurd.
Het effect hieronder is een @Injectable en kan gedeeld worden met andere classes of componenten. Verder wordt er gebruik gemaakt van: Actions uit ngRx en wordt een UserService geïnjecteerd. De code hieronder lijkt in eerste instantie complex, maar kan wel makkelijk gekopieerd en geplakt worden en bestrijkt de meeste gebruikerszaken.
Een effect wordt gemaakt met de createEffect() functie. Daarbinnen wordt dat action geplaatst, gevolgd door een eerste pipe() methode om de data te transformeren. De ofType() functie filtert de action en dat is in dit geval: UserActions.getUsers.
Binnen de mergeMap operator (uit RxJS) wordt een aanroep uitgevoerd naar de Service om alle gebruikers op te halen. Dit laatste wordt gevolgd door een tweede pipe() methode waarmee de data-stream wordt getransformeerd. Binnen de tweede pipe() methode is er zowel een map operator (wanneer er succesvol data is ontvangen) als een catchError operator (wanneer er iets fout is gegaan) – beiden halen de hierbij passende action over.
Ophalen en tonen van data in het component
Met eenmaal de actions, reducers en de effects op zijn plaats is er in het component relatief eenvoudige code te gebruiken om de data op te halen. In het voorbeeld bestaat de data uit gebruikers. Met de dispatch() methode is de action - voor het ophalen van gebruikers - te verzenden. De gebruikers zijn vanuit de store weer op te halen met de select() methode. Zie de afbeelding hieronder:
Het uiteindelijke resultaat is te zien in de afbeelding hieronder. Wanneer eenmaal de gebruikers zijn opgehaald en er teruggegaan wordt naar dezelfde pagina, dan wordt de data (de opgehaalde gebruikers) uit de store gehaald in plaats vanuit de API.
Tot slot
De ngRx library kan dienen om dubbele code wat betreft state management te voorkomen. Bovendien wordt ngRx op dezelfde manier toegepast in projecten. De code ziet overal er hetzelfde uit, zelfs binnen verschillende IT-bedrijven.
De ngRx library is echter niet de enige tool binnen Angular voor state management. Met de release van Angular 19 (op 19 november 2024) zijn er nieuwe features beschikbaar gekomen zoals de Resource API en linkedSignals die state managment mogelijk maken. Daar zal ik in een volgend blogbericht over schrijven.
Als experiment heb ik andere zaken uitgeprobeerd met ngRx en de voorbeeldcode daarvan is te vinden op mijn Github via de link: