In NgRx, what does createActionGroup generate from a source and an events map, and why does a payload-less event need emptyProps()?
answer
- one source, many events
- camel-cased creator names
- [Source] plus event name as type
- props<{}>() is rejected
- a dictionary value cannot be omitted
basics
~20 screateActionGroup 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
Recall the input shape, source plus events, and that each event becomes a camel-cased creator whose type is '[Source] Event Name'.
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.
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.
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.