angular_ngrx_signalstore

Angular NgRx SignalStore

Auteur: Danny Holstein

In een eerder blogbericht uit november 2024 heb ik geschreven over Angular state mangement met NgRx, waarin de ‘normale’ store uitvoerig is behandeld. Hiernaast is er ook een NgRx signalStore die ik recentelijk heb uitgeprobeerd. Wat tijdens het experimenteren met de NgRx signalstore meteen opviel, was dat er aanzienlijk minder bestanden nodig zijn. In dit nieuwe blogbericht zal de NgRx signalStore verder worden behandeld.

Angular 20 en de ‘normale’ store

In bepaalde gebruikerszaken is het nodig dat de state van een applicatie wordt bijgehouden (zoals: paginering, aantal items per pagina, geladen data, etc.) dat ook nog eens gedeeld moet worden met verschillende componenten. Hierbij komt de library NgRx (NgRx = Ng voor Angular + Rx voor Redux) om de hoek kijken.

De ‘normale’ store heeft gescheiden bestanden nodig voor de ations, selectors, reducers en effects. Het toepassen van deze onderdelen kan in eerste instantie complex zijn. Het toevoegen van de NgRx library met de ‘normale’ store is eigenlijk alleen van belang in grote en of complexe applicaties. Voor middelgrote en kleinere applicaties is de ‘normale’ store te veel van het goede. De prestaties zijn weliswaar goed, maar zijn te verbeteren door in componenten gebruik te maken van ChangeDetectionStrategy.OnPush.

Het updaten van de ‘normale’ store is echter zo af en toe wel nodig wanneer er een upgrade wordt gedaan van een Angular applicatie. In versie 20 is het deprecated om strings te gebruiken om een bepaalde store te selecteren met de select functie. Het is aanbevolen om hiervoor een functie (zoals in de voorbeeldcode hieronder) of om een aparte selector te gebruiken.

De voordelen van NgRx signalStore

De NgRx signalStore is ontworpen om de complexiteit van de ‘normale’ store weg te nemen. Beide stores proberen hetzelfde probleem met betrekking tot state mangement in Angular applicaties op te lossen. Beide stores maken gebruik van het Redux-pattern, dat wil zeggen: een enkele bron van waarheid, onwijzigbare (immutable) updates en voorspelbare state-changes. Andere overeenkomsten zijn dat beide stores gebruik maken van TypeScript, reactieve updates in componenten mogelijk maken en zijeffecten kunnen afhandelen.

Echter is de NgRx signalStore alleen beschikbaar vanaf Angular versie 16 (waarin de signals zijn geïntroduceerd). Het is mogelijk om de actions, selectors, reducers en effects in 1 bestand te plaatsen. De benodigde code is beknopter en makkelijker te begrijpen. De reactiviteit gebeurd via simpelere native Angular signals in plaats meer complexe RxJS (Reactive Extensions for JavaScript) Observables. Door het gebruikt van signals zijn de prestaties van de NgRx signalStore eveneens beter, want signals hebben een meer verfijnde reactiviteit en zorgen er niet voor dat de hele tree-of-components ververst hoeft te worden bij een kleine verandering.

De ‘normale’ store en signalStore onder de motorkap

In het vorige blogbericht over Angular state mangement met NgRx is niet de technische werking besproken.  De ‘normale’ store maakt feitelijk gebruikt van een RxJS BehaviourSubject (waarop intern wordt ‘ingeschreven’ met de subscribe functie) en dit type Subject heeft een standaardbuffer van 1 object. Het resultaat van dit laatste is dat alle subscribers de meest recente data met elkaar zullen delen. Onder de motorkap accumuleert de scan operator (uit RxJS) de data voor alle state changes. Onnodige updates worden voorkomen met de operator distinctUntilChanged (eveneens uit RxJS). Wat betreft prestaties worden bepaalde selectors in de ‘normale’ store onthouden. De actions(stream) bestaat feitelijk uit normale RxJS Subjects (op een Subject kan ook op worden ‘ingeschreven’ met de subscribe functie, maar een Subject heeft geen buffer) die alle actions uitzendt.

De signalStore heeft echter een andere werking onder de motorkap. De signalStore is gebouwd op native Angular signals die in Angular 16 zijn geïntroduceerd. Veel gebruikte signals zijn WritableSignals en ComputedSignals. Er is sprake van fijnmazige reactiviteit, aangezien signals exact bijhouden wat er is veranderd. Bovendien worden ook automatisch de dependencies (bijvoorbeeld ComputedSignals die gebruik maken van WritableSignals) bijgehouden waardoor er niet handmatige ‘inschrijvingen’ (met de subscribe functie) nodig zijn. Van nature zijn signals synchroon en zullen updates ook synchroon worden uitgevoerd, dit in tegenstelling tot de veelal asynchrone aard van RxJS.

De bovenstaande tekst is in de afbeelding hieronder schematisch weergegeven:

ngrx_under_the_hood

NgRx signalStore: Code in de praktijk

Voor zowel de ‘normale’ store als signalStore heb ik een kleine Angular applicatie gemaakt waar fictieve persoonsdata wordt geladen en paginering getoond. Al snel bleek dat bepaalde zaken drastisch zijn versimpeld.

De twee benodigde pakketten zijn: @ngrx/store en @ngrx/signals – en beiden zijn te installeren met de 2 commando’s in de terminal:

ng add @ngrx/store
npm install @ngrx/signals

Door middel van de signalStore functie wordt de store aangemaakt. Een default state is in te stellen met de withState functie. In de code hieronder maakt de regel met: “{ providedIn: ‘root’ } “ de store beschikbaar in de hele applicatie. De personsService is direct te injecteren via de withMethods functie en hierdoor is er geen apart bestand nodig voor Effects. Zie de voorbeeldcode hieronder:

Een ander voordeel van de signalStore is dat er voor de properties in initialState hierboven automatisch een signal wordt gemaakt. Deze properties zijn in een component en of HTML-template direct aan te roepen als signal. Wanneer de state verandert, dan zal het component en het HTML-template ook automatisch updaten. Zie de voorbeeldcode hieronder:

ngrx_component_and_template_001

Alle actions worden binnen de withMethods functie geplaatst. Voor selectors heeft de signalStore een speciale withComputed functie die op zijn beurt andere signals maakt die de state automatisch zal updaten. Aparte bestanden voor de reducers zijn vervangen door de patchState functie. In de voorbeeldcode hieronder zijn er 2 actions (updatePageIndex en updatePageSize) die de paginering updaten van de applicatie. Binnen deze 2 actions wordt de store geüpdatet door middel van de patchState functie.

ngrx_example_code_002

In plaats om gescheiden bestanden te maken voor de effects biedt de signalStore twee uitwijkmogelijkheden, namelijk: gebruik maken van de rxMethod utility functies of zelf methoden te gebruiken binnen de withMethods van de signalStore.

In de voorbeeldcode hieronder is voor de eerste optie gekozen. De action: getAllPersons maakt gebruik van de rxMethod functie. Hierbinnen wordt gebruik gemaakt van de pipe functie om de data te transformeren. De RxJS operator switchMap projecteert alleen de eerste data die uitgezonden wordt en zal latere uitzendingen van data negeren. Binnen switchMap wordt de service aangeroepen en de data getransformeerd met een tweede pipe functie. Er kan een succesvolle uitzending (met next) van de data of een fout/error zijn. In beide gevallen wordt de state van applicatie geüpdatet met de patchState functie van de signalStore. Zie de code hieronder:

ngrx_example_code_003

De persoonsdata kan worden geladen door de action: getAllPersons aan te roepen in het component. Wanneer de data is geladen, dan heeft de property isLoading een waarde van false – en deze property is in het HTML-template direct aan te roepen als signal. Zie de code hieronder:

ngrx_component_and_template_002

Tot slot

Voor mij was het verrassend dat er maar 1 bestand nodig is voor de actions, reducers, selectors en effects binnen de signalStore. De hoeveelheid code is aanzienlijk minder, makkelijker te gebruiken, minder complex en sneller onder de knie te krijgen.

De prestaties van de signalStore zijn niet alleen beter door het gebruik van de snellere reactieve signals, maar ook is de bundle size uiteindelijk kleiner doordat niet alle NgRx pakketten nodig zijn.

Mijn experimenten met zowel de ‘normale’ store als de signalStore zijn op Github te vinden.

Experimenten met NgRx signalStore:

https://github.com/dannybee82/ngrx_signals_examples

 

Experimenten met de ‘normale’ NgRx store:

https://github.com/dannybee82/ngrx_tests_tutorial_examples