Prototype of SignalStore plugin for event-based state management.
Event creators are defined using the eventGroup
function:
// users.events.ts
import { emptyProps, eventGroup, props } from '@ngrx/signals/events';
export const usersPageEvents = eventGroup({
source: 'Users Page',
events: {
opened: emptyProps(),
refreshed: emptyProps(),
},
});
export const usersApiEvents = eventGroup({
source: 'Users API',
events: {
usersLoadedSuccess: props<{ users: User[] }>(),
usersLoadedFailure: props<{ error: string }>(),
},
});
The reducer is added to the SignalStore using the withReducer
feature. Case reducers are defined using the when
function:
// users.store.ts
import { when, withReducer } from '@ngrx/signals/events';
export const UsersStore = signalStore(
{ providedIn: 'root' },
withEntities<User>(),
withRequestStatus(),
withReducer(
when(usersPageEvents.opened, usersPageEvents.refreshed, setPending),
when(usersApiEvents.usersLoadedSuccess, ({ users }) => [
setAllEntities(users),
setFulfilled(),
]),
when(usersApiEvents.usersLoadedError, ({ error }) => setError(error)),
),
);
Side effects are added to the SignalStore using the withEffects
feature:
// users.store.ts
import { Events, withEffects } from '@ngrx/signals/events';
export const UsersStore = signalStore(
/* ... */
withEffects(
(
_,
events = inject(Events),
usersService = inject(UsersService),
) => ({
loadUsers$: events
.on(usersPageEvents.opened, usersPageEvents.refreshed)
.pipe(
exhaustMap(() =>
usersService.getAll().pipe(
mapResponse({
next: (users) => usersApiEvents.usersLoadedSuccess({ users }),
error: (error: { message: string }) =>
usersApiEvents.usersLoadedError({ error: error.message }),
}),
),
),
),
logError$: events
.on(usersApiEvents.usersLoadedError)
.pipe(tap(({ error }) => console.log(error))),
}),
),
);
Dispatched events can be listened to using the Events
service.
If an effect returns a new event, it will be dispatched automatically.
State and computed signals are accessed via store instance:
// users.component.ts
@Component({
selector: 'app-users',
standalone: true,
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);
}
Events are dispatched using the Dispatcher
service:
// users.component.ts
import { Dispatcher } from '@ngrx/signals/events';
@Component({
/* ... */
template: `
<h1>Users</h1>
<button (click)="onRefresh()">Refresh</button>
<!-- ... -->
`,
})
export class UsersComponent implements OnInit {
readonly usersStore = inject(UsersStore);
readonly dispatcher = inject(Dispatcher);
ngOnInit() {
this.dispatcher.dispatch(usersPageEvents.opened());
}
onRefresh(): void {
this.dispatcher.dispatch(usersPageEvents.refreshed());
}
}
It's also possible to define self-dispatching events using the injectDispatch
function:
// users.component.ts
import { injectDispatch } from '@ngrx/signals/events';
@Component({
/* ... */
template: `
<h1>Users</h1>
<button (click)="dispatch.refreshed()">Refresh</button>
<!-- ... -->
`,
})
export class UsersComponent implements OnInit {
readonly usersStore = inject(UsersStore);
readonly dispatch = injectDispatch(usersPageEvents);
ngOnInit() {
this.dispatch.opened();
}
}
The reducer can be moved to a separate file using the custom SignalStore feature:
// users.reducer.ts
export function withUsersReducer() {
return signalStoreFeature(
{ state: type<EntityState<User> & RequestStatusState>() },
withReducer(
when(usersPageEvents.opened, usersPageEvents.refreshed, setPending),
when(usersApiEvents.usersLoadedSuccess, ({ users }) => [
setAllEntities(users),
setFulfilled(),
]),
when(usersApiEvents.usersLoadedError, ({ error }) => setError(error)),
),
);
}
The same can be done for effects:
// users.effects.ts
export function withUsersEffects() {
return signalStoreFeature(
withEffects(
(
_,
events = inject(Events),
usersService = inject(UsersService),
) => ({
loadUsers$: events
.on(usersPageEvents.opened, usersPageEvents.refreshed)
.pipe(
exhaustMap(() =>
usersService.getAll().pipe(
mapResponse({
next: (users) => usersApiEvents.usersLoadedSuccess({ users }),
error: (error: { message: string }) =>
usersApiEvents.usersLoadedError({ error: error.message }),
}),
),
),
),
logError$: events
.on(usersApiEvents.usersLoadedError)
.pipe(tap(({ error }) => console.log(error))),
}),
),
);
}
The final SignalStore implementation will look like this:
// users.store.ts
export const UsersStore = signalStore(
{ providedIn: 'root' },
withEntities<User>(),
withRequestStatus(),
withUsersReducer(),
withUsersEffects(),
);