skip to content

An NgRx bookstore app dispatches one shared loadBooks action from the catalogue page, the genre filter and a refresh button; why is that poor action hygiene, and how would you restructure it?

level: seniorimportance: should knowfreq 46%

answer

  1. events, not commands
  2. who dispatched it, and why
  3. a readable DevTools trail
  4. one source, one action
  5. many handlers can share one reaction

basics

~20 s

A shared loadBooks command hides which source fired it, so the DevTools log shows identical entries and handlers cannot treat sources differently. Dispatch one [Source] Event action per source instead, and let the loading reaction listen for all of them.

solid answer

~40 s

`loadBooks` is a command reused from three places, so the DevTools log shows three identical `[Books] Load Books` entries and nobody can tell whether the reader opened the page, changed genre or clicked refresh. It also couples the component to store internals, and handlers can only treat sources differently through payload flags. NgRx's guidance is to capture events, not commands: dispatch `[Catalogue Page] Opened`, `[Genre Filter] Genre Changed` and `[Catalogue Page] Refresh Clicked`, ideally from a `createActionGroup` per source, and have the one loading reaction listen for all three. The same thinking replaces several `dispatch` calls in a row with one combining event. NgRx's ESLint plugin checks both: `good-action-hygiene` and `avoid-dispatching-multiple-actions-sequentially`.

go deeper

for a junior

Recall that NgRx actions should describe events from a named source, like '[Genre Filter] Genre Changed', rather than commands like loadBooks.

for a middle

Explain how a shared command hides its source in the DevTools log and forces payload flags, and how one reaction can listen for several events.

for a senior

Show the restructuring: action groups per source, a single combining event instead of sequential dispatches, and the lint rules that keep the team on it.

for a principal

Weigh the extra action count against debuggability across many features, and set a team convention for which triggers deserve their own events.

## The setup In an online bookstore, three places need the catalogue's book list refreshed: the catalogue page when it opens, the genre filter when a reader picks a genre, and a refresh button. A common first design is one shared action, used as a command: ```ts export const loadBooks = createAction( '[Books] Load Books', props<{ genre?: string }>() ); // catalogue page, genre filter and refresh button all do: this.store.dispatch(loadBooks({ genre })); ``` It works, but NgRx's guidance calls this poor **action hygiene**: the practice of writing actions as unique, descriptive events. ## What goes wrong - **The DevTools log stops telling a story.** The Redux DevTools extension, which NgRx's DevTools connect to, lists every action by type. Three identical `[Books] Load Books` entries cannot say whether the reader opened the page, changed the genre or clicked refresh. Bug reports that start "the list reloaded twice" have no trail to follow. - **Sources cannot be handled differently.** Suppose opening the page should also load the wishlist, while a genre change should reset paging. With one shared action, the handlers must guess from the payload, or the team adds flags such as `fromFilter: true`, which is an event name hidden in the payload. - **The component orchestrates store internals.** `loadBooks` names what the store should do, so the component now knows about loading. The NgRx docs put it as capturing **events, not commands**, separating the description of an event from the handling of that event. - **Sequences creep in.** Once actions are commands, a reset button ends up dispatching `clearFilters()`, `loadBooks()` and `loadWishlist()` in a row, a "transaction" spread across three dispatches. ## Restructuring: one event per source The fix is to dispatch **what happened**, from **where** it happened, and let the store decide what that means: ```ts export const CataloguePageActions = createActionGroup({ source: 'Catalogue Page', events: { Opened: emptyProps(), 'Refresh Clicked': emptyProps(), }, }); export const GenreFilterActions = createActionGroup({ source: 'Genre Filter', events: { 'Genre Changed': props<{ genre: string }>() }, }); ``` The component dispatches `CataloguePageActions.opened()`, `GenreFilterActions.genreChanged({ genre })` or `CataloguePageActions.refreshClicked()`. The one "load books" reaction still exists, but it now **listens for all three events**: NgRx's reducers and effects both accept several action creators for one handler. The reset button dispatches a single `[Catalogue Page] Reset Clicked`, and every part of the store that cares reacts to it. | | Command-style | Event-style | |---|---|---| | Type | `[Books] Load Books` | `[Genre Filter] Genre Changed` | | Says | what the store should do | what happened, and where | | Reused from | many places | exactly one source | | DevTools log | identical entries | a readable trail of user and API events | | Per-source handling | flags in the payload | a separate action per source | ## The rules NgRx writes down The actions guide lists five: write actions **upfront**, **divide** them by source, write **many** (they are cheap), keep them **event-driven**, and make them **descriptive**. Two ESLint rules in NgRx's plugin enforce parts of this: 1. `good-action-hygiene` reports a `createAction` type string that does not match `[Source] Event`. 2. `avoid-dispatching-multiple-actions-sequentially` reports several `dispatch` calls in a row in one block, and recommends one combining event instead. `createActionGroup` makes the source part structural: every type in a group shares its bracketed source. ## The cost, and the judgment call More actions means more names to read. The honest trade: - **Pay it for anything users or APIs trigger.** These are the events you will debug from a log. - **Do not split what is truly one event.** Two buttons that do the same thing on the same page can share an action whose source is the page. - **Do not hide the source in the payload.** A `source: 'filter'` field is invisible in the DevTools action list, which shows types, and every handler must branch on it. ## Reviewing an existing action file A quick audit of an existing feature finds most hygiene problems: 1. List the types in the DevTools log for one user journey, such as opening the catalogue, changing genre and adding a book to the wishlist. 2. Mark every type that appears for more than one user intent. Each one is a shared command to split. 3. Mark every place that dispatches two or more actions in a row. Each one is a missing combining event. A candidate at this level should argue from the debugging trail and from the handlers' freedom to diverge, not from the naming rule alone. The rule is how NgRx writes it down; the log and the handlers are why it matters.

  • Doesn't one action per source multiply the handlers you have to write?
    Not the handlers, only the action definitions. NgRx reducers and effects accept several action creators for one reaction, so the loading logic is written once and lists the three events it responds to. The extra cost is a few definitions per source, which `createActionGroup` keeps short, and the payoff is a readable log and freedom to diverge later.
  • A reset button dispatches clearFilters, loadBooks and loadWishlist in a row; what would you change?
    Dispatch a single `[Catalogue Page] Reset Clicked` event and let each part of the store react to it. The component then reports one fact instead of orchestrating three steps, the log shows one intentional user event, and NgRx's `avoid-dispatching-multiple-actions-sequentially` lint rule stops reporting the block.

A shared loadBooks is a shop bell that rings the same way for every door. Unique events are a bell per door: the clerk still fetches the same stock, but now knows who is waiting and can greet each one differently.

saying these in an interview costs you the question

  • Reusing one action everywhere is better because it means less code to maintain.
  • Put a source field in the payload instead of creating more action types.
  • Actions should be named after what the store should do, like setBooks.
  • Dispatching three actions in a row is fine because each one is small.
  • Action names do not matter once the reducers are tested.