Aprende sobre "Events" en Signal Store
Events en NgRx SignalStore: del acoplamiento directo al patrón Flux
Si ya usas SignalStore con withState(), withComputed() y withMethods(), conoces bien el flujo habitual: un componente inyecta el store, llama a un método, y el store hace su trabajo. Funciona. Pero a medida que la aplicación crece, ese modelo empieza a mostrar sus costuras.
Events es una feature experimental introducida en NgRx v19.2 y que tomó forma más definitiva en v20 (julio 2025). No reemplaza lo que ya tenías — te da una capa adicional para los casos donde el acoplamiento directo se convierte en un problema real.
Source: Announcing NgRx v20 - DEV Community
El problema que viene a resolver
Con withMethods(), el componente tiene que saber qué método del store llamar y en qué orden. Algo así:
// users.component.ts
@Component({ ... })
export class UsersComponent implements OnInit {
private readonly store = inject(UsersStore);
ngOnInit() {
this.store.loadUsers();
}
onRefresh() {
this.store.setLoading();
this.store.loadUsers();
this.store.updateLastRefreshed();
}
}
El componente está orquestando la lógica. Sabe que hay que llamar a setLoading antes de loadUsers, que hay que actualizar lastRefreshed después. Si esa secuencia cambia, hay que tocar el componente.
Con events, el componente simplemente anuncia lo que ocurrió:
// users.component.ts
@Component({ ... })
export class UsersComponent implements OnInit {
readonly dispatch = injectDispatch(usersPageEvents);
ngOnInit() {
this.dispatch.opened();
}
onRefresh() {
this.dispatch.refreshed();
}
}
El componente no sabe nada de lo que ocurre después. El store es quien decide cómo reaccionar.
Source: NgRx Signals Events Documentation
Cómo se define un evento
Los eventos se agrupan con eventGroup(). Se les da una fuente (source) para identificar de dónde vienen, y se declaran con type<Payload>():
// users-page.events.ts
import { eventGroup, type } from '@ngrx/signals/events';
export const usersPageEvents = eventGroup({
source: 'Users Page',
events: {
opened: type<void>(),
refreshed: type<void>(),
userSelected: type<{ userId: string }>(),
},
});
// users-api.events.ts
export const usersApiEvents = eventGroup({
source: 'Users API',
events: {
loadedSuccess: type<User[]>(),
loadedFailure: type<{ error: string }>(),
},
});
Separar los eventos por fuente (UI vs. API) es una convención que ayuda a que el código sea más legible y a que los errores de lógica sean más fáciles de rastrear.
Source: The new Event API in NgRx Signal Store - AngularArchitects
Cómo el store reacciona: withReducer() y on()
Los cambios de estado sincrónicos van en withReducer(). Cada on() conecta uno o varios eventos con una función que devuelve el nuevo estado:
// users.store.ts
import { signalStore, withState } from '@ngrx/signals';
import { withReducer, on } from '@ngrx/signals/events';
import { setAllEntities } from '@ngrx/signals/entities';
import { setPending, setFulfilled, setError } from './request-status.feature';
export const UsersStore = signalStore(
withState({ users: [] as User[] }),
withReducer(
on(usersPageEvents.opened, usersPageEvents.refreshed, setPending),
on(usersApiEvents.loadedSuccess, ({ payload }) =>
setAllEntities(payload)
),
on(usersApiEvents.loadedFailure, ({ payload }) =>
setError(payload.error)
),
),
);
El reducer recibe el evento completo, y de él puedes extraer payload si lo tiene. La función puede devolver un objeto de estado parcial, o un array de actualizaciones si necesitas aplicar varias a la vez.
Source: NgRx Signals Events Documentation
Los efectos: withEffects()
Para operaciones asíncronas (llamadas HTTP, timers, navegación), withEffects() expone un objeto de Observables. El servicio Events actúa como el bus por donde fluyen todos los eventos:
// users.store.ts (continuación)
import { withEffects } from '@ngrx/signals/events';
import { Events } from '@ngrx/signals/events';
export const UsersStore = signalStore(
withState({ users: [] as User[] }),
withReducer(
on(usersPageEvents.opened, usersPageEvents.refreshed, setPending),
on(usersApiEvents.loadedSuccess, ({ payload }) => setAllEntities(payload)),
on(usersApiEvents.loadedFailure, ({ payload }) => setError(payload.error)),
),
withEffects((store, events = inject(Events), usersService = inject(UsersService), dispatcher = inject(Dispatcher)) => ({
loadUsers$: events
.on(usersPageEvents.opened, usersPageEvents.refreshed)
.pipe(
exhaustMap(() =>
usersService.getAll().pipe(
tap((users) => dispatcher.dispatch(usersApiEvents.loadedSuccess(users))),
catchError((error) => {
dispatcher.dispatch(usersApiEvents.loadedFailure({ error: error.message }));
return EMPTY;
}),
)
),
),
})),
);
events.on() devuelve un Observable que emite cada vez que alguno de los eventos listados es despachado. Desde ahí, el flujo RxJS es el de siempre.
Cómo despachar eventos desde un componente
Hay dos formas. La primera es inyectar Dispatcher directamente:
// users.component.ts
const dispatcher = inject(Dispatcher);
dispatcher.dispatch(usersPageEvents.opened());
La segunda, y más cómoda, es injectDispatch(). Recibe el grupo de eventos y devuelve un objeto con un método por cada evento:
// users.component.ts
@Component({
template: `
<button (click)="dispatch.refreshed()">Refresh</button>
<button (click)="dispatch.userSelected({ userId: user.id })">Ver usuario</button>
`
})
export class UsersComponent implements OnInit {
readonly dispatch = injectDispatch(usersPageEvents);
ngOnInit() {
this.dispatch.opened();
}
}
No necesitas inyectar el store en el componente si solo necesitas despachar eventos. El componente no sabe nada del estado ni de cómo se procesa.
Source: Announcing Events Plugin for NgRx SignalStore - DEV Community
Comunicación entre stores
Este es uno de los casos más concretos donde events aporta algo que withMethods() no puede hacer fácilmente. Si tienes dos stores que necesitan reaccionar al mismo evento, ambos pueden escucharlo de forma independiente:
// notifications.store.ts
export const NotificationsStore = signalStore(
withState({ messages: [] as string[] }),
withReducer(
on(usersApiEvents.loadedFailure, ({ payload }) =>
({ messages: [`Error al cargar usuarios: ${payload.error}`] })
),
),
);
El store de notificaciones reacciona al evento de fallo del store de usuarios sin que ninguno sepa de la existencia del otro. Solo comparten los tipos de eventos.
Source: Event-Driven State Management with NgRx Signal Store - DEV Community
Casos de uso donde tiene sentido
Audit logging
Puedes tener un efecto que escuche todos los eventos relevantes y los registre:
// audit.store.ts
withEffects((store, events = inject(Events)) => ({
auditLog$: events
.on(
usersPageEvents.opened,
usersPageEvents.userSelected,
usersApiEvents.loadedSuccess,
usersApiEvents.loadedFailure,
)
.pipe(
tap((event) => console.log('[Audit]', event.type, event)),
),
})),
Undo / Redo
Cada evento es un registro explícito de lo que ocurrió. Si guardas esa secuencia, puedes reproducirla hacia adelante o hacia atrás. Con métodos directos no tienes ese historial de forma natural.
Lógica condicional sin acoplar componentes
Si un evento desencadena comportamientos distintos según el estado actual, esa lógica vive en el reducer o el efecto, no en el componente. El componente siempre hace lo mismo: despacha el evento.
Qué no cambia
Events no reemplaza withMethods(). Si tienes lógica local sencilla — toggle de un booleano, actualización de un campo de filtro — no necesitas montar un grupo de eventos para eso. withMethods() sigue siendo la herramienta correcta para ese tipo de operaciones.
La recomendación del equipo de NgRx es usar events cuando:
La acción puede desencadenar múltiples reacciones (estado + efecto + otro store)
Necesitas trazabilidad explícita de lo que ocurrió
Quieres que los componentes no orquesten la lógica de negocio
Source: NgRx Signals Events Documentation
Una nota sobre el estado experimental
Events entró en estado experimental en NgRx v20. Eso significa que el equipo lo considera listo para usarse, pero las APIs pueden cambiar sin seguir el ciclo estándar de deprecación. Si lo adoptas en producción, conviene que revises el changelog entre versiones.
"The events plugin is experimental. APIs may change in future versions without a standard deprecation cycle." — Source: NgRx v20 Release Notes
Los eventos no se bufferean
Un detalle que vale la pena conocer: los eventos en SignalStore usan Subjects internamente, lo que significa que no se almacenan ni se reproducen. Si un efecto no está activo cuando se despacha un evento, ese evento se pierde. En la práctica esto no suele ser un problema porque los efectos se registran en el mismo ciclo de inicialización del store, pero es algo a tener en cuenta si intentas despachar eventos muy temprano en el bootstrap de la app.
Resumen del flujo completo
Componente
│
│ dispatch.opened()
▼
Dispatcher (bus de eventos)
│
├──► withReducer → on(opened) → setPending() [cambio de estado síncrono]
│
└──► withEffects → events.on(opened) → HTTP request
│
│ dispatcher.dispatch(loadedSuccess(data))
▼
withReducer → on(loadedSuccess) → setAllEntities(data)
El componente solo ve la primera línea. Todo lo demás está encapsulado en el store.
Keywords
NgRx SignalStore Events
withReducer on NgRx
withEffects SignalStore
eventGroup NgRx
injectDispatch NgRx
Meta-description
Aprende cómo funciona el sistema de Events en NgRx SignalStore: eventGroup, withReducer, withEffects e injectDispatch con ejemplos prácticos y casos de uso reales.