Das neue Event API bringt die Arbeitsweise, die man vom Redux-basierten NgRx Global Store kennt, in die Welt des Signal Stores. Da Redux impliziert, dass es nur einen zentralen Store gibt, nutzt das NgRx den allgemeineren Begriff Flux.
Eine der großen Stärken dieses neuen API ist es, dass es sich gezielt für ausgewählte Fälle nutzen lässt. Somit kann man leichtgewichtig starten und bei Bedarf einzelne Stores auf Flux umstellen.
In diesem Artikel zeige ich, wie sich dieses neue API nutzen lässt. Die verwendeten Codebeispiele finden sich unter [1] in den Branches 10b-first-reducer und 10d-redux.
NEU! Generative AI für Angular-Entwickler:innen
Erleben Sie das neue Bootcamp vom 23. – 24. Oktober 2025
Eventing zwischen Stores
Um zu veranschaulichen, dass sich das Event API gezielt einsetzen lässt, demonstriere ich zunächst die Etablierung einer losen Kopplung zwischen zwei Stores. Hier steht zunächst das Eventing an sich und weniger der Einsatz von Flux im Vordergrund. Letzterem wende ich mich weiter unten zu.
Das verwendete Beispiel besteht aus drei Stores: Der DessertStore verwaltet die geladene Liste mit Desserts und für die Bearbeitung eines ausgewählten Desserts kommt der DessertDetailStore zum Einsatz. Außerdem finden sich die Bewertungen der einzelnen Desserts in einem RatingStore (Abb. 1).

Eine solche Aufteilung ist bei leichtgewichtigen Stores wie dem Signal Store nicht unüblich. Im Gegensatz zu Redux erfolgt hier die Aufteilung nicht anhand technischer Aspekte wie Reducer, Effects oder Selectors. Stattdessen wird ein kleiner Teil des Zustands gemeinsam mit darauf basierenden Berechnungen und Operationen gekapselt.
Der verwaltete Zustand entspricht häufig einer Entität aus Sicht eines (Teil-)Features. Diese Aufteilung hat im hier betrachteten Beispiel zur Folge, dass ein vom DessertDetailStore aktualisiertes Dessert ggf. auch im DessertStore aktualisiert werden muss, um die geladene Übersicht aktuell zu halten.
Um zu verhindern, dass beide Stores aneinandergekoppelt werden, bietet sich ein Event an, das die Aktualisierung eines Desserts anzeigt. Solche Events lassen sich mit der Funktion event definieren. Üblicher ist es jedoch, mehrere zusammengehörige Events im Rahmen einer EventGroup bereitzustellen (Listing 1).
Listing 1
import { type } from '@ngrx/signals';
import { eventGroup } from '@ngrx/signals/events';
import { Dessert } from './dessert';
export const dessertDetailStoreEvents = eventGroup({
source: 'Dessert Detail Store',
events: {
dessertUpdated: type<{
dessert: Dessert
}>(),
},
});
Alle Events einer Gruppe haben dieselbe Source. Diese gibt an, welche Teile der Anwendung sie auslösen. Somit lassen sich die Nachrichtenflüsse beim Debuggen besser nachvollziehen. Jedes Event definiert sich durch einen Namen (hier: dessertUpdate) und einem Typ (type) für seine Payload. Diese Payload enthält Daten, die das Event näher beschreiben.
Der DessertDetailStore löst das dessertUpdate Event nach dem Speichern eines Desserts aus. Dazu nutzt er den von der Eventing API bereitgestellten Dispatcher, den sich der Store injizieren lässt (Listing 2).
Listing 2
import { Dispatcher } from '@ngrx/signals/events';
[...]
export const DessertDetailStore = signalStore(
[...]
withProps(() => ({
[...],
_dispatcher: inject(Dispatcher)
})),
withMethods((store) => ({
[...],
save(id: number, dessert: Partial<Dessert>): void {
[...]
// Trigger event
const event = dessertDetailStoreEvents.dessertUpdated({
dessert: savedDessert
});
store._dispatcher.dispatch(event);
},
})),
);
Als Empfänger im DessertStore fungiert ein Reducer. Er nimmt den Payload des Events entgegen und liefert einen Updater zurück (Listing 3).
Listing 3
import { on, withReducer } from '@ngrx/signals/events';
[...]
export const DessertStore = signalStore(
{ providedIn: 'root' },
withState({
[...]
desserts: [] as Dessert[],
}),
withReducer(
on(dessertDetailStoreEvents.dessertUpdated, ({ payload }) => {
const updated = payload.dessert;
return (store) => ({
desserts: store.desserts.map((d) =>
d.id === updated.id ? updated : d,
),
});
}),
),
[...]
)

Angular Trends live in Action?
Das gibt's in den interaktiven Workshops vom 20. - 24. Oktober 2025.
Der Updater aktualisiert das aktualisierte Dessert in der Liste mit sämtlichen abgerufenen Desserts. Der zurückgelieferte Zustand ist partiell, d. h., er muss lediglich die geänderten Eigenschaften enthalten. Wie vom Signal Store gewohnt, lassen sich Updater auch in eigene Funktionen auslagern (Listing 4). Damit lässt sich der Reducer verkürzen (Listing 5).
Listing 4
export type DessertSlice = {
desserts: Dessert[];
};
function updateDessert(updated: Dessert) {
return (store: DessertSlice) => ({
desserts: store.desserts.map((d) =>
(d.id === updated.id ? updated : d)),
});
}
Listing 5
withReducer(
on(dessertDetailStoreEvents.dessertUpdated, ({ payload }) => {
return updateDessert(updated);
}),
),
Diese Möglichkeit kommt auch zum Einsatz, um Updater von Custom Features wie withEntity zu nutzen. Wird kein Updater benötigt, kann der Reducer auch direkt einen aktualisierten partiellen Zustand liefern. Ich greife diese Möglichkeit weiter unten auf.
Flux/Redux für einzelne Stores
Im nächsten Schritt möchte ich zeigen, wie sich das Event API nutzen lässt, um das Flux-Muster für einzelne Stores zu implementieren. Somit ergibt sich die vom Redux-basierten NgRx Global Store gewohnte Arbeitsweise:
-
Der Konsument löst ein Event aus (beim Global Store ist hier von Action die Rede).
-
Effects empfangen Events und stoßen asynchrone Seiteneffekte an. Das Ergebnis veröffentlichen sie in Form weiterer Events.
-
Reducer im Store nehmen Events entgegen und aktualisieren den verwalteten Zustand.
Auch hier gilt, dass man diese Vorgehensweise selektiv anwenden kann und nicht allen Stores aufzwingen muss. Besonders interessant ist dieses Muster, wenn mehrere Komponenten denselben Zustand konsumieren sowie verändern.
Die Implementierung startet wieder mit einer Event Group. Sie enthält ein Event loadDesserts, das einen Effect veranlasst, Desserts zu laden. Dieser Effect informiert daraufhin über den Ausgang dieses Unterfangens mit dem Event loadDessertsSuccess oder im Fehlerfall mit loadDessertsError (Listing 6).
Listing 6
import { eventGroup } from '@ngrx/signals/events';
import { type } from '@ngrx/signals';
import { Dessert } from './dessert';
export const dessertEvents = eventGroup({
source: 'Dessert Feature',
events: {
loadDesserts: type<{
originalName: string,
englishName: string,
}>(),
loadDessertsSuccess: type<{
desserts: Dessert[]
}>(),
loadDessertsError: type<{
error: string
}>(),
},
});
Reducer
Die Reducer für das Laden von Desserts sind etwas simpler als im ersten Fall. Anstatt eines Updaters, der den aktuellen Zustand auf den neuen abbildet, liefern sie lediglich die neuen Werte zurück (Listing 7).
Listing 7
import { on, withReducer } from '@ngrx/signals/events';
[...]
export const DessertStore = signalStore(
{ providedIn: 'root' },
withState({
filter: {
originalName: '',
englishName: '',
},
loading: false,
desserts: [] as Dessert[],
error: '',
}),
withReducer(
[...],
on(dessertEvents.loadDesserts, ({ payload }) => {
return {
filter: payload,
loading: true,
};
}),
on(dessertEvents.loadDessertsSuccess, ({ payload }) => {
return {
desserts: payload.desserts,
loading: false,
};
}),
on(dessertEvents.loadDessertsError, ({ payload }) => {
return {
error: payload.error,
loading: false,
};
}),
),
[...]
);
Effects
Die Effects im Event API, die nichts mit dem gleichnamigen Konzept in der Signals-Welt zu tun haben, sind technisch gesehen RxJS-basierte Pipes, die eingehende Events auf ausgehende Events abbilden. Dazu binden sie in der Regel über einen Flattening-Operator wie switchMap oder exhaustMap eine asynchrone Aktion ein (Listing 8).
Listing 8
import { Events, withEffects } from '@ngrx/signals/events';
import { mapResponse } from '@ngrx/operators';
[...]
export const DessertStore = signalStore(
[...],
withProps(() => ({
_dessertService: inject(DessertService),
_toastService: inject(ToastService),
_events: inject(Events),
})),
[...]
withEffects((store) => ({
loadDesserts$: store._events.on(dessertEvents.loadDesserts).pipe(
switchMap((e) =>
store._dessertService.find(e.payload).pipe(
mapResponse({
next: (desserts) => dessertEvents.loadDessertsSuccess({ desserts }),
error: (error) =>
dessertEvents.loadDessertsError({ error: String(error) }),
}),
),
),
),
})),
);
Auch wenn ein sprechender Name, z. B. loadDesserts$ im gezeigten Fall, wichtig für die Wartbarkeit ist, spielt er technisch gesehen keine Rolle, denn das Event API stößt den Effect nicht über seinen Namen, sondern über Events an. Der Store erzeugt Effects über den Service Event. Seine Methode on filtert die vom Event API empfangenen Events und liefert sie über ein Observable zurück. Übergibt der Store keinen Parameter an on, erhält er sämtliche Events. Übergibt der Store einen oder mehrere Event-Typen, liefert on nur die dazu passenden Events.
Um das Ergebnis der angestoßenen Seiteneffekte auf ein ausgehendes Event abzubilden, bietet sich der mit NgRx gelieferte Operator mapResponse an. Er ist eine Kombination aus map und catchError und erlaubt eine Transformation der Ergebnisse sowie von Fehlern.
Wie in den ersten Tagen des NgRx Global Store ist es essenziell, dass Fehler behandelt werden, z. B. mit catchError oder eben – wie gezeigt – mit mapResponse. Ansonsten schließt RxJS das Observable und weitere Events werden nicht mehr vom Effekt behandelt. Wie üblich gilt es, diese Fehler auch auf der Ebene zu behandeln, auf der sie aufgetreten sind. Im gezeigten Fall ist das innerhalb von switchMap, was zu einer weiteren Verschachtelung führt.
Eine Option wie resubscribeOnError beim Global Store, die Fehler automatisch kompensiert, existiert derzeit noch nicht. Das NgRx-Team denkt jedoch darüber nach.
Stay tuned
Bei neuen Artikel & Eventupdates informiert werden:
Store konsumieren
Um den Store zu konsumieren, lassen sich die einzelnen Komponenten in den Dispatcher injizieren und lösen damit das gewünschte Event aus:
this.#dispatcher.dispatch(
dessertEvents.loadDesserts({
originalName: this.originalName(),
englishName: this.englishName(),
}),
);
Bonus: Redux DevTools
Derzeit kommt der Signal Store mit keiner offiziellen Integration in die Redux DevTools [2], die beim Debuggen einen Blick in den Store erlauben und auch über den Verlauf der einzelnen Zustandsänderungen informieren. Allerdings lässt sich diese Möglichkeit auf der Basis des vom Signal Store gebotenen Erweiterungsmechanismus nachrüsten.
Eine Implementierung dieser Idee findet man im Communitypaket @angular-architects/ngrx-toolkit. Es bietet ein Feature withDevtools, das in den Store aufzunehmen ist (Listing 9).
Listing 9
import { withDevtools } from '@angular-architects/ngrx-toolkit';
[...]
export const DessertStore = signalStore(
[...]
withDevtools('DessertStore')
);
Der übergebene String kommt als Name für den Knoten zum Einsatz, der in den Dev Tools den jeweiligen Store repräsentiert. Wurden die DevTools als Browsererweiterung installiert, lassen sich die im Store hinterlegten Daten sowie deren Verlauf in der Developer-Konsole einsehen (Abb. 2).

Zusammenfassung
Das neue Event API für den NgRx Signal Store erlaubt es, leichtgewichtig zu starten und bei Bedarf gezielt Flux-Muster einzuführen. Mit zusätzlichen Tools wie den Redux DevTools lässt sich das Debugging vereinfachen. Damit liefert das Event API eine flexible Brücke zwischen einfacher Signals-Nutzung und erprobten State-Management-Praktiken aus der Redux-Welt.