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?
answer
- development-only guard rails
- two on, four off
- freeze versus plain-data walk
- zone check and duplicate types
- production ignores the config
basics
~10 sNgRx 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 sThe 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
Recall that NgRx throws in development when you mutate state or actions, and that this protection is switched off in production builds.
Explain which two checks are on by default, what the serializability checks accept and reject, and that the config lives on provideStore only.
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.
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.