For a team standardising on NgRx SignalStore, when should stores react through the Events plugin rather than plain store methods, and what conventions would you set?
answer
- methods are the documented default
- one occurrence, many reacting stores
- indirection and a global bus
- one style per state slice
- scope local stores explicitly
basics
~20 sPlain store methods stay the default; the Events plugin pays off when several stores react to one occurrence or must not know each other. Conventions: one update style per slice, pure reducers, I/O in handlers, scoped dispatchers.
solid answer
~30 sThe NgRx docs treat methods as sufficient for most stores and position the Events plugin for inter-store coordination and decoupled architectures. I adopt events where one occurrence, such as a placed order, must update notifications, cart and order history without the caller knowing those stores, or where store-to-store injection would create cycles. The cost is indirection, more code and a global event bus, so I would set conventions: methods by default; one update style per slice; `eventGroup` per source with past-tense names; pure `withReducer` cases and I/O only in `withEventHandlers`, with failures mapped to events; and `provideDispatcher()` for component-level stores.
code
ts · 18 linesimport { signalStore, type, withState } from '@ngrx/signals';
import { eventGroup, on, withReducer } from '@ngrx/signals/events';
export const checkoutEvents = eventGroup({
source: 'Checkout',
events: { orderPlaced: type<{ orderId: string }>() },
});
// The notifications store opts in; checkout never injects it.
export const NotificationsStore = signalStore(
{ providedIn: 'root' },
withState({ messages: [] as string[] }),
withReducer(
on(checkoutEvents.orderPlaced, ({ payload }, state) => ({
messages: [...state.messages, `Order ${payload.orderId} placed`],
}))
)
);go deeper
Recall that SignalStore methods are the default style and that the Events plugin lets stores react to dispatched events instead of direct calls.
Explain what events decouple, the extra code they bring, and that the dispatcher is global unless a component provides its own scope.
Give concrete triggers for events, such as several stores reacting to one checkout, and the guard-rails that keep reducers pure and handlers alive.
Own the convention: default to methods, adopt events per domain with written reasons, forbid two update styles on one slice, and review cross-scope event traffic.
## The default is plain methods A SignalStore's everyday style is **methods**: a component calls `store.loadPage(2)`, and the method patches state and performs any side effect. The NgRx docs say this default approach is sufficient for most use cases, and that the Events plugin (`@ngrx/signals/events`) excels in more advanced scenarios involving **inter-store coordination** or a deliberately **decoupled architecture**. So the question is not "which is better" but "where does indirection pay for itself". ## When events earn their cost Events describe *what happened*; the dispatcher does not know who reacts. That pays off when: - **Several stores react to one occurrence.** A checkout completing should add a "Order placed" notification, clear the cart and refresh the order history. With methods, the checkout code must know and call three stores. With events, it dispatches one `[Checkout] orderPlaced` event and each store's `withReducer` or `withEventHandlers` decides what to do. - **You want to break dependency cycles.** Store A calling store B that calls store A is a smell with methods; with events neither store injects the other. - **Many sources trigger the same transition.** A notifications store that reacts to web-socket pushes, page opens and a polling timer can treat all of them as events with one reducer. - **Traceability matters.** Every change starts from a named, typed event, which reads like a log of what the user and the server did. ## What events cost - **Indirection.** "Go to definition" on a dispatched event does not lead to the code that handles it; you search for the event creator's usages. - **More code.** Event groups, reducers and handlers replace a single method. - **A shared global bus.** `Dispatcher` and `Events` are global by default, so an event dispatched there reaches every store listening in the global scope. Type strings must stay unique, and a component-local store needs `provideDispatcher()` to get its own scope. - **Testing changes shape.** You test by dispatching events and asserting state, not by calling a method. ## Weighing the two | Criterion | Plain methods | Events plugin | |---|---|---| | One store, one screen | simplest, recommended | overhead with little gain | | Several stores react to one occurrence | caller must know every store | one event, each store opts in | | Readability of control flow | direct call, easy to follow | indirect, follow the event type | | Coupling between stores | stores inject each other | stores share only event creators | | Isolation of local state | natural with component-provided stores | needs `provideDispatcher()` scopes | ## Conventions a lead might set 1. **Default to methods; adopt events per domain, not per mood.** Write down which domains use events (for example notifications and checkout) and why. 2. **One style per slice.** A state slice is changed either by methods or by `withReducer`, never both, so every change has one traceable entry point. 3. **Name events by source.** One `eventGroup` per source, typed `[Source] eventName`, describing what happened (`orderPlaced`), not a command (`showToast`). 4. **Reducers are pure; I/O lives in handlers.** `withReducer` cases only compute state; requests go in `withEventHandlers`, with failures mapped to events via `mapResponse` so a handler stream never dies. 5. **Scope local stores.** Component-level stores that use events get `provideDispatcher()`; forwarding to `parent` or `global` is explicit. 6. **Use current names.** `withEventHandlers`, not `withEffects`, which NgRx 21 renamed. ## Adopting events without a rewrite Events do not have to arrive everywhere at once: 1. Keep existing stores method-driven. 2. When a cross-store reaction appears, define the event group for the **source** that announces it. 3. Let the reacting store add `withReducer` or `withEventHandlers` for that event, for the slices it owns. 4. Move a slice fully to events only when a second or third source starts changing it. This keeps each migration step small and reviewable, and avoids the half-and-half slice that the conventions forbid. ## How to answer in an interview A strong answer states the default (methods), names the trigger for events (coordination across stores, decoupling, many sources), accepts the cost (indirection, a global bus, more code) and ends with guard-rails that keep a mixed codebase readable. A weak answer picks one style for everything — "events everywhere, like Redux" or "events are never needed" — without saying what problem it solves. There is no single right answer; the judgment is in matching the style to how many stores must react to the same thing.
- Your team uses events for notifications but a teammate also adds a markAllRead() method that calls patchState on the same items slice. What do you say?Two entry points now change one slice, so neither the event log nor the reducers tell the whole story of how items changed. Pick one style for the slice: either dispatch a `markedAllRead` event handled by `withReducer`, or keep the whole slice method-driven. The rule is per slice, not per store, so other slices may still use methods.
- How do you stop a component-level store's events from reaching root stores in other parts of the app?Provide `provideDispatcher()` next to the store in the component's `providers`. Events dispatched in that subtree stay local by default, while the local scope still hears parent and global events. Anything that must leave the scope is forwarded explicitly with `{ scope: 'parent' }` or `{ scope: 'global' }`, which makes cross-scope traffic visible in review.
A method call is phoning one colleague: you must know who handles the job. An event is an announcement over the office speaker: you say what happened, and whoever cares acts. Announcements shine when many people must react, but you cannot tell from the announcement who did.
saying these in an interview costs you the question
- Every SignalStore should use events so the codebase looks like Redux
- Events are never needed because stores can inject each other
- Store methods cannot be asynchronous, so any request needs an event handler
- Mixing methods and reducers on the same state slice is harmless
- Events dispatched in a component stay local without any extra provider