Custom Feature
Oftmals haben wir wiederkehrende Strukturen oder Funktionen, die in mehreren Stores verwendet werden sollen. Für diese Zwecke bietete RxJs die Möglichkeit ein so genanntes Custom Feature zu erstellen, das Stores dann nutzen können, um diese Funktionen und Attribute zu inkludieren. Hier ein Beispiel, wie sich auf einfache Art und Weise ein Event-System in Stores inkludieren lässt:
In einer separaten Datei (z.B. store.utils.ts) definieren wir die Datenstrukturen, die wir verwenden werden. In unserem Beispiel ein Event Objekt mit einem Namen und optionalen zusätzlichen Daten:
interface EventState<T, E> {
event: { name: T, data?: E } | null,
}
export enum CrudStoreEvent {
UPDATE = 'UPDATE',
DELETE = 'DELETE',
CREATE = 'CREATE'
}
NgRx bietet uns nun eine entsprechende Methode signalStoreFeature, die es uns erlaubt Methoden und Attribute zu definieren, genau wie in unseren Stores auch. In unserem Beispiel definieren wir eine generische Methode, die zum einen den EventTyp definiert um ggf. auch fachlich konkrete Events definieren zu können und ein optionaler Typ für die Definition der Nutzlast. Neben dem Event selber fügen wir noch eine eigene Patch-Methode hinzu, um die Verwendung in den Stores zu vereinfachen:
export function withCrudEvent<T = CrudStoreEvent, E = unknown>() {
return signalStoreFeature(
withState<EventState<T, E>>({event: null}),
withMethods(store => ({
patchEvent: (event: T, data?: E) => patchState(store, {event: {name: event, data: data}}),
}))
);
}
Innerhalb konkreter Stores ist die Verwendung dann ganz einfach: Extension registrieren
export const CocktailStore = signalStore(
withState(initialState),
withCrudEvent(), // << Extension registrieren
withMethods((
store,
cocktailService = inject(CocktailResourceService)
) => ({
createCocktail: rxMethod<Cocktail>(
pipe(
switchMap((cocktail) => cocktailService.apiPublicCocktailsPost(cocktail).pipe(
tapResponse({
next: () => store.patchEvent(CrudStoreEvent.CREATE), // << nutzen
error: (error) => {
console.log(error)
},
})
)),
)
),
Oder innerhalb der Komponenten
constructor() {
effect(() => {
const event = this.cocktailStore.event();
if (event?.name === CrudStoreEvent.CREATE) {
...
}
}
Entity
Entity Management ist eine smarte Lösung, um im lokalen State, Collections von Objekten über ihre Id zu verwalten, um z.B. in komplexen Formularen Zuordnungen und Listen zu verwalten. Dazu muss lediglich das Feature aktiviert werden:
export const CocktailStore = signalStore(
withEntities<Cocktail>()
...
)
Standardmäßig muss hier ein „Id“ Attribut im Entity-Type vorliegen (number | string), alternativ lässt sich aber auch eine angepasste Id definieren. Das Feature stellt uns nun diverse Methoden und Attribute zur Verfügung, um unsere Entities zu verwalten:
// anlegen
patchState(store, addEntities<Cocktail>(
[
{id: 1, name: 'Mojito'},
{id: 2, name: 'Whiskey'}
]
));
// update
patchState(store, updateEntity<Cocktail>(({id: 2, changes: {name: 'Whiskey Updated'}})));
// auslesen
const cocktails: Cocktail[] = store.entities();
const cocktail: Cocktail = store.entityMap()[2];
Testing
Der vollständigkeitshalber ein kurzer Blick aufs Testing. Das ist dank der Kapselung unseres States denkbar einfach:
describe('CocktailStore', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
// standards
provideZonelessChangeDetection(),
provideHttpClient(),
provideHttpClientTesting(),
// unsere services mocken die normalerweise unsere Rest-Aufrufe durchführen
provideCocktailResourceServiceMockProvider(),
// Store providen
CocktailStore
]
});
});
// store Methoden aufrufen und prüfen
describe("Store-Test", () => {
it("should load cocktails", () => {
const store = TestBed.inject(CocktailStore)
store.loadCocktails();
expect(store.cocktails().length).toEqual(1);
})
})
});






