The newly introduced Event API transfers the familiar mechanics of the Redux-based NgRx "Global" Store into the Signal Store domain. Redux traditionally assumes a single central store, which is why the NgRx team adopts the broader notion of Flux instead.
A notable advantage of this API is its selective applicability. It enables a lightweight entry point, letting teams incrementally introduce the Flux pattern into specific stores only.
This guide demonstrates how to work with this new API.
📂 Example Code
🔀 Branches: 10b-first-reducer and 10d-redux
Communication Through Events Between Stores
To highlight the potential of the Event API for targeted usage, I'll first show how to establish a loose coupling between two stores. The primary focus here is on eventing itself, not on a complete Flux implementation. Following that, I'll explain how to introduce Flux within a chosen store.
The demo setup includes three stores: the DessertStore handles the list of desserts that have been fetched, the DessertDetailStore is responsible for manipulating a currently selected dessert, and the RatingStore keeps ratings for individual desserts:

This breakdown is typical of lightweight stores like the Signal Store. Unlike Redux, the division isn't made along technical dimensions such as reducers, effects, or selectors. Instead, a focused piece of state is bundled together with the operations and computations that depend on it.
In many cases, the managed state mirrors an entity from the perspective of a particular (sub)feature. In the scenario at hand, this implies that a dessert modified via the DessertDetailStore may also need to be synchronized within the DessertStore to keep the displayed overview current.
To avoid tight coupling between these stores, firing an event that signals a dessert update is a sensible approach. Such events can be declared with the Event API's event function. In practice, however, it's more typical to group related events together within an Event Group:
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
}>(),
},
});
Each event within a group carries the same source. The source identifies which part of the application emitted the event, simplifying the tracing of message flows during debugging. Every event is defined by a name (here: dessertUpdate) and a type for its payload. That payload carries data that provides further context about the event.
Once a dessert has been saved, the DessertDetailStore emits the dessertUpdate event. To accomplish this, it relies on the Dispatcher service that the Eventing API provides:
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);
},
})),
);
On the receiving end within the DessertStore, a Reducer reacts to the event. It takes the event’s payload and returns an Updater:
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,
),
});
}),
),
[…]
)
The updater swaps the modified dessert in the list with all loaded desserts. The returned state is only partial, meaning it needs to include just the properties that changed. As usual with the Signal Store, Updaters can also be extracted into dedicated functions:
export type DessertSlice = {
desserts: Dessert[];
};
function updateDessert(updated: Dessert) {
return (store: DessertSlice) => ({
desserts: store.desserts.map((d) =>
(d.id === updated.id ? updated : d)),
});
}
With such an Updater function, the reducer can be written more concisely:
withReducer(
on(dessertDetailStoreEvents.dessertUpdated, ({ payload }) => {
return updateDessert(updated);
}),
),
This same approach is used to hand off to Updaters that come from custom features, like withEntity. When the current state isn't needed, the reducer can provide an updated partial state directly. More detail on that option appears later in this post.
Going Deeper: Angular Architecture Workshop (Remote, Interactive, Advanced)
Level up your skills for building enterprise-scale, maintainable Angular applications through our Angular Architecture workshop!
English Version | German Version
Flux/"Redux" for Selected Stores
Next, I'll demonstrate how the Event API can establish the Flux pattern within individual stores. This has close parallels to how the Redux-oriented NgRx “Global” Store works:
- A consumer dispatches an Event (known as an Action in the “Global” Store).
- Effects respond to Events and carry out asynchronous side effects, publishing results as subsequent Events.
- Reducers within the store listen to Events and adjust the state they manage.
Again, the Event API and the Flux approach can be applied on a case-by-case basis; there's no requirement to impose it across every store. This matters especially when several components read and modify the same state.
Just as before, the construction begins with an Event Group. Within it, a loadDesserts event triggers an Effect responsible for fetching desserts. The outcome is then announced through either the loadDessertsSuccess event, or the loadDessertsError event when something goes wrong:
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
The reducers responsible for loading desserts turn out to be simpler than the earlier case. Rather than providing an updater that maps the current state into a new one, they simply return the fresh values directly:
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
Effects within the Event API share no relationship with the concept carrying the same name in the Signals ecosystem. They are essentially RxJS-based pipes that receive incoming events and produce outgoing ones. To achieve this, they usually incorporate an asynchronous operation through a flattening operator like switchMap or mergeMap:
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) }),
}),
),
),
),
})),
);
Although giving an Effect a descriptive name, such as loadDesserts$ in this example, enhances readability, the name carries no technical weight. The Event API activates an Effect based on events, not by its name. The store builds Effects by injecting the Event service. Its on method filters the incoming events and exposes them through an Observable. When the store calls on without arguments, it receives every event. Supplying one or more event types causes on to return only the events that match.
To convert the result of the triggered side effect into an outgoing event, the mapResponse operator included with NgRx proves handy. It bundles the behavior of map and catchError into one operator, allowing both successful results and errors to be transformed.
Recalling the early stages of the NgRx Global Store, error handling is indispensable, whether through catchError or, as demonstrated, with mapResponse. The alternative leads to RxJS closing the observable, after which the Effect ignores further events. Errors must be dealt with at the exact point where they arise. In the presented scenario, that location is within switchMap, which accounts for the added nesting.
The Global Store offers an option called resubscribeOnError that automatically recovers from errors, but no equivalent exists in the Event API at present. The NgRx team, however, is giving it thought.
Using the Store from Components
To interact with the store, each component can inject the Dispatcher and use it to initiate the desired event:
this.#dispatcher.dispatch(
dessertEvents.loadDesserts({
originalName: this.originalName(),
englishName: this.englishName(),
}),
);
Bonus: Redux DevTools
The Signal Store ships without an official integration for Redux Dev Tools, which offer a window into the store during development and also reveal the history behind individual state changes. Fortunately, this capability can be integrated via the Signal Store's extensibility hooks.
A practical realization of this idea exists in the community package @angular-architects/ngrx-toolkit. It offers a feature named withDevtools that can be attached to the store:
import { withDevtools } from '@angular-architects/ngrx-toolkit';
[…]
export const DessertStore = signalStore(
[…]
withDevtools('DessertStore')
);
The string passed in serves as the label for the node that corresponds to the specific store within the Dev Tools. Assuming the Dev Tools have been installed as a browser extension, the data contained in the store along with its complete history become viewable in the Developer Console:

Wrapping Up
With the Event API in the NgRx Signal Store, Flux patterns can be introduced precisely where they're needed. Additional resources like the Redux DevTools further simplify the debugging experience. The Event API forms a flexible middle ground between the lightweight nature of the Signal Store and the proven state management techniques originating from Redux.
