skip to content

In NGXS 22, how do @Selector methods and createSelector memoize, and how should a standalone component read them now that the @Select decorator is deprecated?

level: middleimportance: must knowfreq 34%

answer

  1. static methods, cached results
  2. last arguments only
  3. container state not injected
  4. select() hands back a signal
  5. the decorator bypassed DI

basics

~20 s

@Selector marks a static method whose result is cached on its last inputs; createSelector builds the same kind of selector at runtime. Components read them with select(), which returns a signal, or Store.select for an Observable; @Select is deprecated.

solid answer

~50 s

In NGXS a selector is a static method decorated with `@Selector()`. With no arguments it receives its own state's model; with `@Selector([RoomsState, ReservationsState.today])` it receives exactly those inputs, because since NGXS 18 the container state is not injected by default (`injectContainerState: false`). Each selector is memoized on its last arguments, compared with `Object.is`, so it recomputes only when an input reference changes. `createSelector([RoomsState], fn)` builds a selector at runtime, typically in a factory taking a parameter; every call makes a new selector with its own cache. To read state, a component calls `select(RoomsState.freeRooms)` in an injection context and gets a `Signal`; `Store.selectSignal` does the same, `Store.select` returns an Observable, `selectSnapshot` a one-off value. The `@Select` property decorator is deprecated because it kept the Store in a static reference outside Angular DI, which breaks with several apps on a page.

code

ts · 29 lines
ts
import { Component, Injectable } from '@angular/core';
import { Selector, State, createSelector, select } from '@ngxs/store';

export interface Room { id: string; floor: number; status: 'free' | 'booked'; }
export interface RoomsStateModel { rooms: Room[]; }

@State<RoomsStateModel>({ name: 'rooms', defaults: { rooms: [] } })
@Injectable()
export class RoomsState {
  @Selector()
  static freeRooms(state: RoomsStateModel): Room[] {
    return state.rooms.filter(r => r.status === 'free');
  }

  static roomsOnFloor(floor: number) {
    return createSelector([RoomsState], (state: RoomsStateModel) =>
      state.rooms.filter(r => r.floor === floor)
    );
  }
}

@Component({
  selector: 'app-free-rooms',
  template: `<p>{{ freeRooms().length }} free, {{ groundFloor().length }} on floor 0</p>`
})
export class FreeRoomsComponent {
  freeRooms = select(RoomsState.freeRooms);
  groundFloor = select(RoomsState.roomsOnFloor(0));
}

go deeper

for a junior

Know that @Selector goes on a static method, that select() gives a signal and Store.select an Observable, and that @Select is the old deprecated form.

for a middle

Explain last-arguments memoization with Object.is, why container state is no longer injected, and the difference between lazy and createSelector-based dynamic selectors.

for a senior

Diagnose selectors that recompute on every action, spot createSelector factories rebuilt inside computed or templates, and plan the @Select migration with its SSR motivation.

for a principal

Set where selectors live, such as query classes, and a rule that components never derive store data ad hoc, so memoization and testing stay in one layer.

## Defining selectors versus consuming them NGXS separates two roles. **Defining** a selector means writing a reusable, memoized function that derives data from state. **Consuming** it means a component or service asking the store for that derived value. Confusing the two is the root of most selector bugs. ## @Selector: static, memoized, explicit inputs A selector is a **static** method decorated with `@Selector`. It can live on the state class, on a dedicated query class, or on any plain class. ```ts @State<RoomsStateModel>({ name: 'rooms', defaults: { rooms: [] } }) @Injectable() export class RoomsState { @Selector() static freeRooms(state: RoomsStateModel): Room[] { return state.rooms.filter(r => r.status === 'free'); } } export class FrontDeskQueries { @Selector([RoomsState.freeRooms, ReservationsState.arrivalsToday]) static roomsNeeded(free: Room[], arrivals: Reservation[]) { return arrivals.length - free.length; } } ``` - With **no arguments** on a state class, the method receives that state's own model. - With an **array of inputs**, it receives exactly those, in order. Inputs can be state classes, state tokens or other selectors. - Since NGXS 18 the **container state is not injected** by default: the global option `injectContainerState` defaults to `false`. A selector on `RoomsState` declared `@Selector([ReservationsState])` receives only the reservations model; list `RoomsState` explicitly if you need it. The option is marked "to be deprecated" and exists for migrating NGXS 3 code. - Selector errors are **thrown** by default (`suppressErrors: false`), rather than turned into `undefined`. - Both options can be overridden for one class or one method with the `@SelectorOptions` decorator, or globally through the options object passed to `provideStore`. Overriding `suppressErrors` to hide a crashing selector usually just moves the bug into the template, which then renders `undefined`. ## How the memoization works Each selector keeps **one cached result and the arguments that produced it**. On the next evaluation NGXS compares each new argument with the previous one using `Object.is`. If all match, the cached result is returned without running the function. | Situation | Recomputes? | |---|---| | An unrelated state slice changes | No: inputs kept their references | | The rooms slice gets a new reference | Yes, even if its contents are equal | | A no-op state operator returned the same reference | No | This is why immutable updates that preserve untouched references matter: a hand-written spread that rebuilds `rooms` on every action invalidates `freeRooms` every time. ## Parameterised selectors: lazy and dynamic Two patterns take arguments: 1. **Lazy selector**: the `@Selector` returns a function, for example `(floor: number) => Room[]`. The outer function is memoized; the consumer calls the returned function, often inside a `computed()`. 2. **Dynamic selector**: a static factory calls `createSelector([RoomsState], (s: RoomsStateModel) => s.rooms.filter(r => r.floor === floor))`. Each call creates a **new selector with its own memoization**, even for the same argument. Create it once, as a field, rather than inside a template expression or a `computed()` that runs repeatedly. NGXS also offers helpers that build selectors for you: `createPropertySelectors`, `createModelSelector` and `createPickSelector`. ## Reading state in a standalone component | API | Returns | Typical use | |---|---|---| | `select(selector)` | `Signal<T>` | Component fields; must run in an injection context | | `createSelectMap({...})` | Object of signals | Several selectors at once | | `Store.selectSignal(selector)` | `Signal<T>` | Same as `select()`, with an injected `Store` | | `Store.select(selector)` | `Observable<T>` | RxJS pipelines, the `async` pipe | | `Store.selectOnce(selector)` | `Observable<T>` taking one value | Guards | | `Store.selectSnapshot(selector)` | `T` | Interceptors, one-off reads | `select()` and `selectSignal` accept only **typed selectors or state tokens**; an anonymous `state => state.rooms.list` is not allowed, and `Store.select` also requires a typed selector since NGXS 18. ## Why @Select is deprecated The old way was a property decorator: `@Select(RoomsState.freeRooms) freeRooms$!: Observable<Room[]>`. It is **deprecated since NGXS 18**, still exported in 22 (a removal attempted in the 20 line was reverted in 20.0.2). The decorator could not use Angular's dependency injection, so it kept the `Store` in a static variable. With several applications on one page, as in server-side rendering or micro frontends, another app could overwrite or clear that variable. The migration is mechanical: - `@Select(RoomsState.freeRooms) freeRooms$` becomes `freeRooms$ = inject(Store).select(RoomsState.freeRooms)`, or better `freeRooms = select(RoomsState.freeRooms)` for a signal. - A string or anonymous-function argument must first become a real `@Selector`.

  • In NGXS, why can a selector that returns state.rooms.filter(...) still cause extra change detection?
    Memoization protects you only while the inputs keep their references. If an action rebuilds the `rooms` array even though nothing changed, the selector runs again and `filter` returns a new array, so every signal and `OnPush` view reading it updates. Fix it at the write side with reference-preserving updates; NGXS 22 also adds a dev option, `warnOnNewReferenceWithIdenticalValue`, that reports a `selectSignal` selector returning a new reference with an identical value.
  • When would you still use Store.select instead of the select() function in NGXS 22?
    When the consumer is an RxJS pipeline: combining the value with other streams, debouncing it, or feeding an API that expects an Observable. `Store.select` returns an Observable that emits on each distinct result. For template bindings and `computed()` derivations, the signal from `select()` is the simpler fit, and it avoids manual subscriptions.

saying these in an interview costs you the question

  • A selector on a state class always receives that state's model first, whatever inputs it lists.
  • Calling a createSelector factory twice with the same argument reuses one cached selector.
  • The @Select decorator was removed in NGXS 20 and no longer compiles.
  • select() and selectSignal accept any anonymous function over the root state.
  • Memoized selectors compare their inputs deeply, so an equal but new array skips recomputation.