skip to content

In NgRx, what does createActionGroup generate from a source and an events map, and why does a payload-less event need emptyProps()?

level: middleimportance: must knowfreq 55%

answer

  1. one source, many events
  2. camel-cased creator names
  3. [Source] plus event name as type
  4. props<{}>() is rejected
  5. a dictionary value cannot be omitted

basics

~20 s

createActionGroup returns one action creator per event, named by camel-casing the event name, with type '[Source] Event Name'. A payload-less event needs emptyProps() because the events map needs a value and props<{}>() fails NgRx's empty-object type check.

solid answer

~40 s

`createActionGroup({ source, events })` returns a dictionary of ordinary action creators that share one source. Each key in `events` becomes a creator named by camel-casing it, so `'Genre Changed'` becomes `genreChanged`, and each action type is `'[Catalogue Page] Genre Changed'`, keeping the original case. Values are `props<T>()`, a creator function, or `emptyProps()`. `emptyProps()` exists because a dictionary entry cannot be left out, and `props<{}>()` is rejected by the typings as an empty object; it gives a creator you call with no arguments. The group also fails to compile when two events would produce the same creator name, or when `source` is not a string literal.

go deeper

for a junior

Recall the input shape, source plus events, and that each event becomes a camel-cased creator whose type is '[Source] Event Name'.

for a middle

Explain the three payload forms, why props<{}>() is rejected and emptyProps() is not, and which mistakes the group catches at compile time that createAction does not.

for a senior

Show you would organise actions as one group per source, know the group's duplicate check stops at one call, and know what covers duplicates across the app.

for a principal

Treat action groups as a team convention tool: they make the naming rules structural, so review time goes to event design rather than string typos.

## What `createActionGroup` does `createActionGroup` from `@ngrx/store` builds several action creators that share one **source** in a single call. It takes a config object with two keys: - `source`: the string that goes inside the brackets of every generated type, such as `'Catalogue Page'`; - `events`: a dictionary whose keys are **event names** and whose values describe each event's payload. It returns a dictionary of ordinary action creators, the same kind `createAction` makes. For an online bookstore: ```ts import { createActionGroup, emptyProps, props } from '@ngrx/store'; import { Book } from './book.model'; export const CataloguePageActions = createActionGroup({ source: 'Catalogue Page', events: { Opened: emptyProps(), 'Genre Changed': props<{ genre: string }>(), 'Wishlist Toggled': (bookId: string) => ({ bookId }), }, }); export const BooksApiActions = createActionGroup({ source: 'Books API', events: { 'Books Loaded Success': props<{ books: Book[] }>(), 'Books Loaded Failure': props<{ error: string }>(), }, }); ``` ## How names and types are generated Two strings come out of every event name: | Event key | Creator name | Action type | |---|---|---| | `Opened` | `opened` | `[Catalogue Page] Opened` | | `'Genre Changed'` | `genreChanged` | `[Catalogue Page] Genre Changed` | | `'Wishlist Toggled'` | `wishlistToggled` | `[Catalogue Page] Wishlist Toggled` | | `'Books Loaded Success'` | `booksLoadedSuccess` | `[Books API] Books Loaded Success` | 1. The **creator name** is the event name camel-cased: the first word lower-cased, each later word capitalised, spaces removed. 2. The **action type** is `` `[${source}] ${eventName}` ``, with the event name's original case kept. So `CataloguePageActions.genreChanged({ genre: 'poetry' })` returns `{ genre: 'poetry', type: '[Catalogue Page] Genre Changed' }`. The group also removes the need for a barrel file of named exports: a component imports `CataloguePageActions` and reaches every creator through it. The docs also allow camel-case event keys, such as `booksLoadedSuccess`, so the creator name matches the key exactly and is easier to search for. The type then reads `[Books API] booksLoadedSuccess`. What a group cannot do is give a creator a name that differs from its event name. ## The three payload forms, and why `emptyProps` exists Each value in `events` must say what the creator takes: - `props<{ genre: string }>()` declares a typed payload object; - a **creator function**, such as `(bookId: string) => ({ bookId })`, takes arguments and shapes the payload; - `emptyProps()` declares an event with **no payload**, so the creator is called with no arguments: `CataloguePageActions.opened()`. An events dictionary has no way to say "no value", so a payload-less event still needs something on the right-hand side. The obvious guess, `props<{}>()`, fails to compile: NgRx's `props` typings reject an empty object with "action creator props cannot be an empty object". `emptyProps()` is the explicit spelling for that case. With plain `createAction` you simply omit the second argument; `emptyProps` exists because a dictionary entry cannot be omitted. ## Compile-time checks the group adds `createActionGroup` catches mistakes that `createAction` lets through: - **`source` must be a string literal type.** A variable typed `string` fails with "source must be a string literal type", because the generated types would lose their literal form. - **Event names must be string literals** too. - **No two events in a group may produce the same creator name.** `'Book Added'` and `'book Added'` both camel-case to `bookAdded`, and the compiler reports "bookAdded action is already defined". - **Payload rules still apply**: no arrays, no `type` property, no empty object. Copying a `createAction` line and forgetting to edit its type string compiles fine; the docs call this out as the case the group fixes within one source. Across separate `createAction` calls, catching a repeated type is a runtime check's job, and that check is off by default. ## When to reach for it - Use a group **per source**: one for the catalogue page, one for the books API, one for the wishlist button. That keeps the `[Source] Event` convention by construction. - Keep `createAction` for a one-off event, or where you need a creator name that differs from the event text. - Prefer `emptyProps()` over inventing a dummy payload such as `props<{ noop: true }>()`; a fake field shows up in every logged action and misleads the reader. A candidate who can explain the generated names, the preserved type string and the reason for `emptyProps` has understood what the group is: a typed factory for `createAction` calls that share a source.

  • When would you keep using createAction instead of a group?
    For a one-off event whose source has no other actions, or when the creator must have a name that differs from its event text, which a group cannot do because it always derives the name by camel-casing. Otherwise a group per source is the tidier default: it enforces `[Source] Event` and catches name collisions inside the source at compile time.
  • Does createActionGroup stop two different groups from producing the same action type?
    No. Its duplicate check only compares events inside one call. Two groups with the same `source` and event text would produce identical types and still compile. Catching that across the app is the job of the store's action-type uniqueness runtime check, which is off unless you enable it.

saying these in an interview costs you the question

  • Action types from a group come out as 'catalogue-page/genre-changed', slice style.
  • props<{}>() and emptyProps() are interchangeable; emptyProps is only a naming style.
  • createActionGroup returns a special object, so reducers need a different on() for it.
  • The group prevents duplicate action types across the whole application.
  • emptyProps() makes an action skip reducers and reach only effects.