skip to content

In NGXS, how do ctx.setState and ctx.patchState differ, and when should an action handler use state operators such as patch and updateItem instead?

level: middleimportance: should knowfreq 30%

answer

  1. replace versus merge
  2. only the top level is merged
  3. setState also takes a function
  4. @ngxs/store/operators
  5. unchanged parts keep their reference

basics

~10 s

setState replaces the whole slice and also accepts a state operator; patchState shallow-merges top-level properties into an object slice. State operators like patch and updateItem express nested immutable updates and keep unchanged references intact.

solid answer

~50 s

In NGXS, `ctx.setState(value)` replaces the entire slice, and it also accepts a function of the existing state, which is what a state operator is. `ctx.patchState(partial)` copies the slice and overwrites only the top-level keys you pass, so a nested object you pass replaces the old one wholesale; it is meant for object slices, and in development builds NGXS throws if the value you pass is an array or a primitive. Both return `void`. For nested or array updates, state operators from `@ngxs/store/operators` are the idiom: `patch` for objects (its properties can themselves be operators), `updateItem`, `insertItem`, `append` and `removeItem` for arrays, `iif` and `compose` for logic, plus `safePatch`, `updateItems` and `removeItems` added in NGXS 22. Operators such as `patch` and `updateItem` copy only what changes and, on a no-op, return the existing reference, so memoized selectors do not recompute.

code

ts · 31 lines
ts
import { Injectable } from '@angular/core';
import { Action, State, StateContext } from '@ngxs/store';
import { patch, removeItem, updateItem } from '@ngxs/store/operators';

export interface Reservation { id: string; roomId: string; status: 'pending' | 'confirmed'; }
export interface ReservationsStateModel { items: Reservation[]; filter: { floor: number | null; nights: number } }

export class ConfirmReservation {
  static readonly type = '[Front Desk] Confirm Reservation';
  constructor(public reservationId: string) {}
}
export class CancelReservation {
  static readonly type = '[Front Desk] Cancel Reservation';
  constructor(public reservationId: string) {}
}

@State<ReservationsStateModel>({ name: 'reservations', defaults: { items: [], filter: { floor: null, nights: 1 } } })
@Injectable()
export class ReservationsState {
  @Action(ConfirmReservation)
  confirm(ctx: StateContext<ReservationsStateModel>, { reservationId }: ConfirmReservation) {
    ctx.setState(patch<ReservationsStateModel>({
      items: updateItem<Reservation>(r => r.id === reservationId, patch<Reservation>({ status: 'confirmed' }))
    }));
  }

  @Action(CancelReservation)
  cancel(ctx: StateContext<ReservationsStateModel>, { reservationId }: CancelReservation) {
    ctx.setState(patch<ReservationsStateModel>({ items: removeItem<Reservation>(r => r.id === reservationId) }));
  }
}

go deeper

for a junior

Remember that setState replaces the slice, patchState merges top-level keys, and that state must never be mutated in place.

for a middle

Explain the shallow merge, why patchState only fits object slices, and how patch with nested updateItem expresses a deep update without spreading every level.

for a senior

Connect operators to performance: reference-preserving no-ops keep memoized selectors and signals quiet, and developmentMode freezing catches accidental mutation before it ships.

for a principal

Set team conventions: operators for nested and array updates, patchState only for flat fields, and a review rule against hand-written spreads that churn references.

## Two ways to write from an action handler In NGXS every `@Action` handler receives a **`StateContext`**, an object scoped to one state's slice of the global store. Two of its methods write: | Method | Accepts | Effect on the slice | |---|---|---| | `setState(value)` | A full new value, or a function `(existing) => next` | Replaces the whole slice | | `patchState(partial)` | An object with some top-level keys | Copies the slice, overwrites those keys | Both return `void` in current NGXS; that changed in the 18 release, so code that chained on their return value belongs to an older major. ## What patchState really does `patchState` is a convenience for the common case of an object slice with a few top-level fields. Internally it spreads the existing slice into a new object and assigns each key you passed. Three consequences follow: - **The merge is shallow.** With a slice `{ reservations, filter: { floor, nights } }`, calling `ctx.patchState({ filter: { floor: 3 } })` replaces `filter` entirely, so `nights` is gone. - **It always creates a new top-level object**, even when the values you pass are identical, so anything comparing the slice by reference sees a change. - **It is meant for object slices.** In development builds NGXS throws `Patching arrays is not supported.` or `Patching primitives is not supported.` when the value you pass is an array or a primitive. `patchState` is typed to take a partial object, not a state operator. ## setState with a function, and what a state operator is `setState` can take a function that receives the current slice and returns the next one. A **state operator** is simply such a function, usually produced by a factory. NGXS ships a set of them in `@ngxs/store/operators`: - `patch(spec)`: sets properties on an object; each property can be a value **or another operator**, which is how nested updates stay readable. - `safePatch(spec)` (new in 22): like `patch`, but treats a `null` or `undefined` slice as an empty object. - `append(items)` and `insertItem(value, beforePosition?)`: add to arrays. - `updateItem(indexOrPredicate, valueOrOperator)` and, new in 22, `updateItems`: change matching elements. - `removeItem(indexOrPredicate)` and, new in 22, `removeItems`: remove elements. - `iif(condition, whenTrue, whenFalse?)` and `compose(...operators)`: branch and combine. ```ts ctx.setState( patch<ReservationsStateModel>({ items: updateItem<Reservation>( r => r.id === reservationId, patch<Reservation>({ status: 'confirmed' }) ) }) ); ``` ## Why operators matter for performance The built-in operators copy **only what changes**. `patch` clones an object only if at least one property actually gets a different value; `updateItem` returns the original array when the predicate finds nothing or the new element is identical; `append` with no items returns the existing array. Returning the same reference is the signal that nothing changed, so: 1. Memoized `@Selector` functions whose inputs kept their references are not recomputed. 2. Signals from `select()` and `selectSignal` compare by reference and do not notify. 3. `OnPush` components bound to untouched parts do not re-render. A hand-written spread such as `{ ...state, items: state.items.map(...) }` produces a new array even when no element changed, which invalidates every selector downstream. ## Immutability is still your job NGXS does not copy state for you when you mutate it. `ctx.getState().items.push(r)` changes the array in place without producing a new reference, so selectors and signals see no change. With the `developmentMode: true` store option, NGXS freezes state and actions in development, which turns such mutations into errors you notice early. The operators are the idiomatic way to write immutable updates without spreading every level by hand; the docs also show a curried Immer `produce` passed to `setState`, since it has the same signature as a state operator. ## Choosing between them - Top-level fields on an object slice: `patchState` is fine. - Replacing the slice outright, or a slice that is an array: `setState`, often with `append`, `updateItem` or `removeItem`. - Nested objects or items inside arrays: `setState(patch({ ... }))` with nested operators. - Conditional updates: `iif`, so the decision reads the live state rather than a value captured earlier. - A nested object that may still be `null`, such as an optional guest profile on a reservation: `safePatch` inside `patch`, which treats the missing object as an empty one instead of throwing. Whichever you choose, keep one style per state class; mixing hand-written spreads and operators in the same handler makes it hard to see which updates preserve references.

  • In NGXS, why prefer updateItem over mapping the array yourself when confirming one reservation?
    A hand-written `items.map(...)` always returns a new array, even if no reservation matched, so every selector reading `items` recomputes and every signal built on it notifies. `updateItem` copies the array only when the element actually changes and otherwise returns the existing reference, so a no-op confirm costs nothing downstream.
  • What does NGXS's iif operator add over an if statement in the handler?
    `iif(condition, whenTrue, whenFalse)` can take a predicate that receives the live slice at the moment the operator runs, so the decision is made on current state rather than on a value you read earlier. It also composes inside `patch`, so a conditional nested update stays one declarative expression; when the else branch is omitted, the value stays unchanged.

saying these in an interview costs you the question

  • patchState deep-merges nested objects, so sibling fields inside them are kept.
  • patchState works on array slices and appends the values passed.
  • Mutating the array from getState() is fine because NGXS copies state afterwards.
  • State operators always clone the whole slice to guarantee immutability.
  • setState and patchState return the new state, so handlers can chain on them.