We are pleased to announce the introduction of the Events plugin for NgRx SignalStore, released as part of NgRx v19.2! This new addition brings the power of event-based state management to SignalStore, enabling developers to create scalable applications more effectively.
Experimental Stage 🚧
The Events plugin is currently marked as experimental. This means its APIs are subject to change, and modifications may occur in future versions without standard breaking change announcements until it is deemed stable. We encourage you to try it out, provide feedback, and help shape its future!
Flux Architecture 🔄
This plugin takes inspiration from the original Flux architecture and incorporates the best practices and patterns from NgRx Store, NgRx Effects, and RxJS.
The application architecture with the Events plugin is composed of the following building blocks:
- Event: Describes an occurrence within the system. Events are dispatched to trigger state changes and/or side effects.
- Dispatcher: An event bus that forwards events to their corresponding handlers in the stores.
- Store: Contains reducers and effects that manage state and handle side effects, maintaining a clean and predictable application flow.
- View: Reflects state changes and dispatches new events, enabling continuous interaction between the user interface and the underlying system.
By dispatching events and reacting to them, the what (the event that occurred) is decoupled from the how (the state changes or side effects that result), leading to predictable data flow and more maintainable code.
Walkthrough 🛠️
The Events plugin provides a set of APIs for defining and handling events in a reactive and declarative manner. The following example demonstrates how these APIs can be used to implement state and behavior orchestration through events.
Defining Events
Events are created using the eventGroup
function, which organizes related events under a common source. This function accepts an object with two properties:
-
source
: identifies the origin of the event group (e.g.,'Users Page'
,'Users API'
). -
events
: a dictionary of named event creators, where each key defines the event name and each value defines the payload type using thetype
helper from the core@ngrx/signals
package.
import { type } from '@ngrx/signals';
import { eventGroup } from '@ngrx/signals/events';
export const usersPageEvents = eventGroup({
source: 'Users Page',
events: {
opened: type<void>(),
refreshed: type<void>(),
},
});
export const usersApiEvents = eventGroup({
source: 'Users API',
events: {
loadedSuccess: type<User[]>(),
loadedFailure: type<string>(),
},
});
Performing State Changes
To handle state changes in response to events, SignalStore provides the withReducer
feature. Case reducers are defined using the on
function, which maps one or more events to a case reducer function. Each case reducer receives the event and returns the state update. It supports three forms of return values: a partial state object, a partial state updater, and an array of partial state objects and/or updaters.
import { signalStore } from '@ngrx/signals';
import { setAllEntities, withEntities } from '@ngrx/signals/entities';
import { on, withReducer } from '@ngrx/signals/events';
export const UsersStore = signalStore(
{ providedIn: 'root' },
withEntities<User>(),
withRequestStatus(),
withReducer(
on(usersPageEvents.opened, usersPageEvents.refreshed, setPending),
on(usersApiEvents.loadedSuccess, ({ payload }) => [
setAllEntities(payload),
setFulfilled(),
]),
on(usersApiEvents.loadedFailure, ({ payload }) => setError(payload)),
),
);
💡 Implementation of the
withRequestStatus
feature and its state updaters is available in the official SignalStore documentation.
Performing Side Effects
Side effects are handled using the withEffects
feature. This feature accepts a function that receives the store instance as an argument and returns a dictionary of effects. Each effect is defined as an observable that reacts to specific events using the Events
service. If an effect emits a new event, that event will be automatically dispatched.
import { Events, withEffects } from '@ngrx/signals/events';
import { mapResponse } from '@ngrx/operators';
export const UsersStore = signalStore(
/* ... */
withEffects(
(
store,
events = inject(Events),
usersService = inject(UsersService),
) => ({
loadUsers$: events
.on(usersPageEvents.opened, usersPageEvents.refreshed)
.pipe(
exhaustMap(() =>
usersService.getAll().pipe(
mapResponse({
next: (users) => usersApiEvents.loadedSuccess(users),
error: (error: { message: string }) =>
usersApiEvents.loadedFailure(error.message),
}),
),
),
),
logError$: events
.on(usersApiEvents.loadedFailure)
.pipe(tap(({ payload }) => console.log(payload))),
}),
),
);
Reading State
The plugin doesn’t change how the state is exposed or consumed. Therefore, components can access state and computed signals by using the store instance.
@Component({
selector: 'app-users',
template: `
<h1>Users</h1>
@if (usersStore.isPending()) {
<p>Loading...</p>
}
<ul>
@for (user of usersStore.entities(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UsersComponent {
readonly usersStore = inject(UsersStore);
}
Dispatching Events
To initiate state changes or side effects, events can be dispatched using the Dispatcher
service. It provides a dispatch
method that takes an event as input.
import { Dispatcher } from '@ngrx/signals/events';
@Component({
/* ... */
template: `
<h1>Users</h1>
<button (click)="refresh()">Refresh</button>
`,
})
export class UsersComponent {
readonly usersStore = inject(UsersStore);
readonly dispatcher = inject(Dispatcher);
constructor() {
this.dispatcher.dispatch(usersPageEvents.opened());
}
refresh(): void {
this.dispatcher.dispatch(usersPageEvents.refreshed());
}
}
For convenience, self-dispatching events can be generated using the injectDispatch
function, which returns an object with methods matching the provided event creators.
import { injectDispatch } from '@ngrx/signals/events';
@Component({
/* ... */
template: `
<h1>Users</h1>
<button (click)="dispatch.refreshed()">Refresh</button>
`,
})
export class UsersComponent {
readonly usersStore = inject(UsersStore);
readonly dispatch = injectDispatch(usersPageEvents);
constructor() {
this.dispatch.opened();
}
}
Scaling Up
As applications grow in complexity, reducers and effects can be extracted into standalone features to improve maintainability.
Extracted Reducer
export function withUsersReducer() {
return signalStoreFeature(
{ state: type<EntityState<User> & RequestStatusState>() },
withReducer(
on(usersPageEvents.opened, usersPageEvents.refreshed, setPending),
on(usersApiEvents.loadedSuccess, ({ payload }) => [
setAllEntities(payload),
setFulfilled(),
]),
on(usersApiEvents.loadedFailure, ({ payload }) => setError(payload)),
),
);
}
Extracted Effects
export function withUsersEffects() {
return signalStoreFeature(
withEffects(
(
store,
events = inject(Events),
usersService = inject(UsersService),
) => ({
loadUsers$: events
.on(usersPageEvents.opened, usersPageEvents.refreshed)
.pipe(
exhaustMap(() =>
usersService.getAll().pipe(
mapResponse({
next: (users) => usersApiEvents.loadedSuccess(users),
error: (error: { message: string }) =>
usersApiEvents.loadedFailure(error.message),
}),
),
),
),
logError$: events
.on(usersApiEvents.loadedFailure)
.pipe(tap(({ payload }) => console.log(payload))),
}),
),
);
}
Final Store Composition
export const UsersStore = signalStore(
{ providedIn: 'root' },
withEntities<User>(),
withRequestStatus(),
withUsersReducer(),
withUsersEffects(),
);
Key Takeaways 🔑
The Events plugin:
- Combines proven patterns from Flux, NgRx Store, NgRx Effects, and RxJS.
- Seamlessly integrates with existing SignalStore features.
- Extends Flux architecture with powerful customization options.
- Unifies local and global state management with a single approach.
Upcoming NgRx Workshops 🎓
With NgRx usage continuing to grow with Angular, many developers and teams still need guidance on how to architect and build enterprise-grade Angular applications. We are excited to introduce upcoming workshops provided directly by the NgRx team!
We're offering one to three full-day workshops that cover the basics of NgRx to the most advanced topics. Whether your teams are just starting with NgRx or have been using it for a while - they are guaranteed to learn new concepts during these workshops.
Visit our workshops page for more details. The next workshops are scheduled for:
- June 11-13 (EU Time) | Registration
- September 24-26 (US Time) | Registration
- October 8-10 (EU Time) | Registration
Thanks to All Our Contributors and Sponsors! 🏆
NgRx continues to be a community-driven project. Design, development, documentation, and testing - all are done with the help of the community. Visit our community contributors section to see every person who has contributed to the framework.
If you are interested in contributing, visit our GitHub page and look through our open issues, some marked specifically for new contributors. We also have active GitHub discussions for new features and enhancements.
We want to give a big thanks to our Gold sponsor, Nx! Nx has been a longtime promoter of NgRx as a tool for building Angular applications and is committed to supporting open-source projects that they rely on.
We want to thank our Bronze sponsor, House of Angular!
Lastly, we also want to thank our individual sponsors who have donated once or monthly.
Sponsor NgRx 🤝
If you are interested in sponsoring the continued development of NgRx, please visit our GitHub Sponsors page for different sponsorship options, or contact us directly to discuss other sponsorship opportunities.
Follow us on Twitter and LinkedIn for the latest updates about the NgRx platform.
Top comments (9)
Yeah, what a great feature!! Thanks!
For a future NGRX article, would love to see how Signal Store could combine with the new angular
httpResource
😉Awesome!!! I love this plugin!
by using
on
we don't have the access to the prev state. Now only the payload is availableIt's possible of course. Check the "Performing State Changes" section:
Example:
Any chance a couple of example with updateEntity or setEntity can be added to the article?
I'm fighting with this but keep getting type errors.
The state updater needs to be returned directly to the case reducer handler as a result. In your example, the state updater is returned from another state updater. To fix it, you should execute the updater returned by the
setEntity
function:This can be simplified if the case reducer handler has the current state as the second argument:
If you want, feel free to open a feature request for this in the NgRx repo: github.com/ngrx/platform/issues
I'm such a noob XD thank you <3
In large apps that are slowly migrating from NgRx Store to NgRx Signal Store, is there a method to get a Signal Store Effect to listen to NgRx Store Actions?
I know it's early days, but if the team can't provide this, many people will create a lot of ugly code.
Triggering effects:
provideEnvironmentInitializer(() => inject(RouterStore))