skip to content

In NgRx 22, which of the store's six runtime checks are on by default, what does each one catch, and when would you enable the others?

level: seniorimportance: should knowfreq 26%

answer

  1. development-only guard rails
  2. two on, four off
  3. freeze versus plain-data walk
  4. zone check and duplicate types
  5. production ignores the config

basics

~10 s

NgRx 22 enables strictStateImmutability and strictActionImmutability by default in development; the two serializability checks, strictActionWithinNgZone and strictActionTypeUniqueness start off. Production builds switch all six off, whatever the config says.

solid answer

~40 s

The runtime checks are meta-reducers that throw in development when a reducer rule is broken. In NgRx 22 two are on by default: `strictStateImmutability` deep-freezes each state a reducer returns, and `strictActionImmutability` freezes each action before reducers see it, so mutating either throws. Four are off: `strictStateSerializability` and `strictActionSerializability` throw on values that are not plain objects, arrays or primitives, such as a `Date`, a `Map` or a class instance; `strictActionWithinNgZone` throws when an action is dispatched outside Angular's zone; `strictActionTypeUniqueness` throws when two `createAction` calls register the same type string. You configure them in `provideStore(reducers, { runtimeChecks: { ... } })`, and in production builds NgRx ignores that object and turns every check off. I enable the serializability pair when state is persisted or actions are replayed, and type uniqueness in large codebases.

go deeper

for a junior

Recall that NgRx throws in development when you mutate state or actions, and that this protection is switched off in production builds.

for a middle

Explain which two checks are on by default, what the serializability checks accept and reject, and that the config lives on provideStore only.

for a senior

Choose which off-by-default checks to enable for a codebase that persists state or replays actions, and recognise the NgZone check as harmful in zoneless apps.

for a principal

Weigh development-time strictness against the cost of freezing and walking large states, and decide how the team compensates for checks that never run in production.

## What runtime checks are NgRx's global Store relies on rules it cannot enforce through types alone: reducers must not mutate state, actions must not be mutated, and ideally everything in the store is plain data. **Runtime checks** turn those rules into errors during development. Each check is implemented as a **meta-reducer**, a wrapper around the root reducer, that NgRx installs for you when you call `provideStore`. The checks exist to shorten the feedback loop: a violation throws at the line that caused it, instead of surfacing weeks later as a view that stopped updating. ## The six checks and their defaults | Check | Development default | What throws | |---|---|---| | `strictStateImmutability` | **on** | writing to state after a reducer returned it, because it was deep-frozen | | `strictActionImmutability` | **on** | writing to an action's properties, because it was frozen before reducers ran | | `strictStateSerializability` | off | the state a reducer returns contains a value that is not plain data | | `strictActionSerializability` | off | a dispatched action contains a value that is not plain data | | `strictActionWithinNgZone` | off | an action is dispatched while not inside Angular's zone | | `strictActionTypeUniqueness` | off | the same action type string was registered more than once | Some mechanics behind the table: - The two **immutability** checks use `Object.freeze` recursively. The freeze itself never throws; the error comes later, when some code tries to write to the frozen object, including a component that mutates an array it selected. - The **serializability** checks walk the value and accept primitives, `null`, `undefined`, arrays and plain objects (whose prototype is `Object.prototype` or `null`). A `Date`, `Map`, `Set`, class instance or function throws with the path to the offending property. At 22.0.1 the walk accepts an array as a whole without inspecting its elements, so it is a guard rail, not a proof. - Actions whose type starts with `@ngrx` are exempt from the action checks, so NgRx's own lifecycle actions never trip them. - **Type uniqueness** counts registrations made by `createAction`, which `createActionGroup` also uses, and checks the count when the root store and each feature are set up. ## Production turns them all off NgRx decides the active checks with `isDevMode()`: 1. In development it starts from the defaults above and applies your `runtimeChecks` overrides. 2. In production it returns `false` for **all six** and ignores your configuration entirely. So you cannot "keep" a check in production, and a clean development run is the only place these rules are verified. The upside is that deep-freezing and serializability walks, which are costly on large states, never slow production. ## Configuring them Runtime checks are a **root** setting: they are part of the config accepted by `provideStore` (or the older `StoreModule.forRoot`), not by `provideState`. ```ts provideStore(reducers, { runtimeChecks: { strictStateSerializability: true, strictActionSerializability: true, strictActionTypeUniqueness: true, }, }); ``` Omitted keys keep their defaults, so this keeps both immutability checks on. ## When to enable the four that are off - **Serializability** when state is persisted, for example hydrated from storage by a meta-reducer, or when actions are recorded and replayed. A `Date` for `completedAt` in the admin orders slice is the classic catch; store an ISO string instead. The docs warn that these checks conflict with `@ngrx/router-store`'s `FullRouterStateSerializer`, whose router state is not serializable; use the minimal serializer or a custom one. - **Type uniqueness** in large codebases where copy-pasted action definitions can collide, which would make two unrelated reducers react to the same action. - **Within-NgZone** only in zone-based applications that dispatch from third-party callbacks running outside the zone, where the view would not refresh. In a **zoneless** application, Angular's `NgZone.isInAngularZone()` is always false, so enabling this check makes every application action throw. ## Key points - **Two on, four off** in development; **all off** in production. - Immutability checks **freeze**; serializability checks **walk and reject non-plain values**. - Configure through `runtimeChecks` on **`provideStore`** only. - Leave the NgZone check off in zoneless apps.

  • The immutability check is on, yet a component mutated an order it selected and nothing threw; how can that happen?
    The check only runs in development, so a production build never freezes anything. In development the likeliest cause is that the component mutated a copy it made itself, or a value it got from somewhere other than the store. Frozen state throws when module code, which runs in strict mode, writes to it, so a write that truly reached store state would have thrown.
  • Why would a team enable strictStateSerializability, and what does it cost?
    It proves the state is plain data that survives JSON persistence and replay, which matters when a meta-reducer hydrates state from storage or when actions are recorded. It costs a walk of the returned state on every action in development, and it forces types such as `Date` or `Map` to become strings, arrays or plain objects in the model.

saying these in an interview costs you the question

  • All six runtime checks are enabled by default in development.
  • Setting a runtime check to true in provideStore keeps it active in production builds.
  • strictStateImmutability throws at the moment a Date is stored in state.
  • Runtime checks can be configured per feature in provideState's config.
  • strictActionWithinNgZone is harmless to enable in a zoneless application.