7. BONUS: Using the NGRX Signal Store
🔀 Branch: For this lab, please switch to the branch 08-signal-store-starter:
git reset --hard
git checkout 08-signal-store-starter
In this lab, we switch from our hand-made store to the NGRX Signal Store.
7.1 Switching to the Signal Store
Open the file dessert.store.ts (src/app/data/dessert.store.ts). It contains a new version of our store using the NGRX Signal Store. Try to find out which properties and methods it provides.
7.2 Using RxJS with the Signal Store
Now, let's use the Signal Store's RxJS-interop layer to implement a typeahead.
-
Open the file
dessert.store.ts(src/app/data/dessert.store.ts) and add anrxMethodcalledloadDessertsByFilter:import { computed, inject } from '@angular/core'; +import { tapResponse } from '@ngrx/operators'; import { patchState, signalStore, withComputed, + withHooks, withMethods, withState, } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { debounceTime, filter, pipe, switchMap, tap } from 'rxjs'; import { ToastService } from '../shared/toast'; import { Dessert } from './dessert'; import { DessertFilter } from './dessert-filter'; [...] withState({ filter: { originalName: '', - englishName: '', + englishName: 'Cake', }, loading: false, ratings: {} as DessertIdToRatingMap, [...] updateFilter(filter: DessertFilter): void { patchState(store, { filter }); }, - loadDesserts(): void { - patchState(store, { loading: true }); - - dessertService.find(store.filter()).subscribe({ - next: (desserts) => { - patchState(store, { desserts, loading: false }); - }, - error: (error) => { - patchState(store, { loading: false }); - toastService.show('Error loading desserts!'); - console.error(error); - }, - }); - }, loadRatings(): void { patchState(store, { loading: true }); [...] }, })); }, + loadDessertsByFilter: rxMethod<DessertFilter>( + pipe( + filter( + (f) => f.originalName.length >= 3 || f.englishName.length >= 3, + ), + debounceTime(300), + tap(() => patchState(store, { loading: true })), + switchMap((f) => + dessertService.find(f).pipe( + tapResponse({ + next: (desserts) => { + patchState(store, { desserts, loading: false }); + }, + error: (error) => { + toastService.show('Error loading desserts!'); + console.error(error); + patchState(store, { loading: false }); + }, + }), + ), + ), + ), + ), }), ), + withHooks({ + onInit(store) { + const filter = store.filter; + store.loadDessertsByFilter(filter); + }, + }), );Remarks: Make sure the
rxMethodis signaled whenever the search filter changes. For this, you can add anonInithook.Further remarks: The call to
filteranddebounceTimecould be moved to the component to make the store more reusable. For the sake of simplicity, we keep it here during this lab. -
Switch to the file
desserts.component.ts(src/app/desserts/desserts.component.ts) and remove all calls toloadDessertsas loading is now directly triggered by a filter change:export class DessertsComponent { ratedDesserts = this.#store.ratedDesserts; loading = this.#store.loading; - constructor() { - this.#store.loadDesserts(); - } - - search(): void { - this.#store.loadDesserts(); - } - loadRatings(): void { this.#store.loadRatings(); } -
Open the file
desserts.component.html(src/app/desserts/desserts.component.html) and remove theSearchbutton:</div> </div> <div class="mt-10 mb-10"> - <button - type="submit" - (click)="search()" - class="btn btn-primary" - [disabled]="loading()" - > - Search - </button> - <button - type="button" - (click)="loadRatings()" - class="btn ml-2" - [disabled]="loading()" - > + <button type="button" (click)="loadRatings()" class="btn"> Expert Ratings </button> -
Try out your solution.
💾 Please find this lab's solution in the branch 08a-signal-store-rxjs (get reset --hard && git checkout 08a-signal-store-rxjs). You don't have to switch to it branch, if you are fine.
7.3 Bonus: Adding a Custom Signal Store Feature
One of the strengths of the Signal Store is the possibility to create reusable features for repeating requirements. In this example, we move some of our logic for loading entities like desserts to a reusable custom feature.
🔀 Branch: For this lab, please switch to the branch 09-custom-feature-starter:
git reset --hard
git checkout 09-custom-feature-starter
-
Have a look to the
DessertStore(src/app/data/dessert.store.ts).Remarks: The store was refactored so that all features (
withState,withMethods,withHooks) that deal with desserts and are about to be moved into a reusable feature are located before the first comment. -
Crate a file
data-service.feature.ts(src/app/data/data-service.feature.ts) and add a custom feature grouping the before mentioned features:import { inject } from '@angular/core'; import { tapResponse } from '@ngrx/operators'; import { patchState, signalStoreFeature, withHooks, withMethods, withState, } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { debounceTime, filter, pipe, switchMap, tap } from 'rxjs'; import { ToastService } from '../shared/toast'; import { Dessert } from './dessert'; import { DessertFilter } from './dessert-filter'; import { DessertService } from './dessert.service'; export function withDataService() { return signalStoreFeature( withState({ filter: { originalName: '', englishName: 'Cake', }, loading: false, desserts: [] as Dessert[], }), withMethods( ( store, dessertService = inject(DessertService), toastService = inject(ToastService), ) => ({ updateFilter(filter: DessertFilter): void { patchState(store, { filter }); }, loadDessertsByFilter: rxMethod<DessertFilter>( pipe( filter( (f) => f.originalName.length >= 3 || f.englishName.length >= 3, ), debounceTime(300), tap(() => patchState(store, { loading: true })), switchMap((f) => dessertService.find(f).pipe( tapResponse({ next: (desserts) => { patchState(store, { desserts, loading: false }); }, error: (error) => { toastService.show('Error loading desserts!'); console.error(error); patchState(store, { loading: false }); }, }), ), ), ), ), }), ), withHooks({ onInit(store) { const filter = store.filter; store.loadDessertsByFilter(filter); }, }), ); } -
Switch to the file
dessert.store.ts(src/app/data/dessert.store.ts) and replace the discussed features with our custom feature:import { computed, inject } from '@angular/core'; -import { tapResponse } from '@ngrx/operators'; import { patchState, signalStore, withComputed, - withHooks, withMethods, withState, } from '@ngrx/signals'; -import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { debounceTime, filter, pipe, switchMap, tap } from 'rxjs'; import { ToastService } from '../shared/toast'; -import { Dessert } from './dessert'; -import { DessertFilter } from './dessert-filter'; -import { DessertService } from './dessert.service'; +import { withDataService } from './data-service.feature'; import { DessertIdToRatingMap, RatingService } from './rating.service'; import { toRated } from './to-rated'; export const DessertStore = signalStore( { providedIn: 'root' }, - withState({ - filter: { - originalName: '', - englishName: 'Cake', - }, - loading: false, - desserts: [] as Dessert[], - }), - withMethods( - ( - store, - dessertService = inject(DessertService), - toastService = inject(ToastService), - ) => ({ - updateFilter(filter: DessertFilter): void { - patchState(store, { filter }); - }, - loadDessertsByFilter: rxMethod<DessertFilter>( - pipe( - filter( - (f) => f.originalName.length >= 3 || f.englishName.length >= 3, - ), - debounceTime(300), - tap(() => patchState(store, { loading: true })), - switchMap((f) => - dessertService.find(f).pipe( - tapResponse({ - next: (desserts) => { - patchState(store, { desserts, loading: false }); - }, - error: (error) => { - toastService.show('Error loading desserts!'); - console.error(error); - patchState(store, { loading: false }); - }, - }), - ), - ), - ), - ), - }), - ), - withHooks({ - onInit(store) { - const filter = store.filter; - store.loadDessertsByFilter(filter); - }, - }), + + withDataService(), // // Ratings-realated parts have been moved down -
Try our your modifications
💾 Please find this lab's solution in the branch 09a-add-custom-feature (get reset --hard && git checkout 09a-add-custom-feature). You don't have to switch to it branch, if you are fine.
7.4 Bonus: Generic Custom Feature
Now, let's inspect a generic version of our custom feature.
🔀 Branch: For this lab, please switch to the branch 09a-add-custom-feature:
git reset --hard
git checkout 09a-add-custom-feature
-
Have a look at the file
data-service.feature.ts(src/app/data/data-service.feature.ts). It contains a generic version of our feature. -
Open the file
dessert.store.ts(src/app/data/dessert.store.ts) and see how the generic feature is called.
> Follow us on X (Twitter) > More Trainings (German) > More Trainings (English)