In NgRx 22, why should an invoicing dashboard's components read the invoice slice through selectors instead of mapping store state themselves?
answer
- one place knows the state shape
- derived once, shared by every reader
- last arguments, last result
- pure functions, composable inputs
- prefer-selector-in-select
basics
~20 sSelectors keep knowledge of the state shape in one file, compute derived values such as invoice totals once and cache them, compose into larger reads, and are pure functions that can be tested without a component.
solid answer
~40 sA selector is a pure function from the store state to the value a view needs. Writing `createSelector(selectInvoices, (invoices) => ...)` once gives every component the same outstanding total or overdue count, so a renamed field is fixed in one file instead of in every template. Selectors built with `createSelector` and `createFeatureSelector` remember their last arguments and result, so the total is recalculated only when the invoices array itself changes, not on every dispatched action. They compose: a per-customer view is built from the invoice selector and the selected-customer-id selector. Being pure, they are tested as plain functions. Components consume them with `store.select(...)` for an observable or `store.selectSignal(...)` for a signal, and the NgRx ESLint rules `prefer-selector-in-select` and `avoid-mapping-selectors` flag reads that bypass them.
code
ts · 16 linesimport { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectOutstandingTotal, selectOverdueCount } from './invoices.selectors';
@Component({
selector: 'app-invoice-header',
template: `
<span>Outstanding: {{ outstanding() }}</span>
<span>Overdue: {{ overdue() }}</span>
`,
})
export class InvoiceHeaderComponent {
private readonly store = inject(Store);
readonly outstanding = this.store.selectSignal(selectOutstandingTotal);
readonly overdue = this.store.selectSignal(selectOverdueCount);
}go deeper
Recall the four reasons: one place knows the state shape, derived values are cached, selectors compose, and pure functions are easy to test. Name createSelector and the two ways a component reads one.
Explain what the cache actually holds, the last arguments and result, and why an action that does not touch invoices leaves the total uncomputed. Contrast select and selectSignal as consumers of the same selector.
Show you keep derivations out of components and out of reducers: selectors own derived data, reducers own stored data. Mention the ESLint rules that enforce it on a team.
Frame selectors as the read model of the store: a stable, versionable API between state shape and views, which is what lets a team refactor the reducers without touching dozens of components.
## What a selector is In NgRx the **Store** holds one immutable state tree, and a **selector** is a pure function that takes that tree and returns the piece a view needs: `(state) => value`. `@ngrx/store` ships two helpers for building them. `createFeatureSelector<InvoicesState>('invoices')` reads one top-level key of the state. `createSelector(input1, input2, projector)` runs its **input selectors** against the state and passes their results to a **projector** function that derives the final value. A component never reads the tree directly. It hands a selector to the `Store` and receives either an observable (`store.select(selector)`) or an Angular signal (`store.selectSignal(selector)`). ## What goes wrong when components map state themselves Imagine an invoicing dashboard whose header shows the outstanding total, whose sidebar shows the overdue count, and whose main panel lists the selected customer's invoices. If each component writes `this.store.select((state) => state.invoices.items.filter(...))` itself: - **The state shape leaks everywhere.** Renaming `items` to `entities` means editing every component that touched it. - **The same derivation runs many times.** Three widgets that each filter and sum the invoices do the work three times per state change. - **Nothing is cached.** An inline function has no memory, so the filter reruns on every state emission, and a filtered array is a new reference each time, so the view receives a "changed" value even when nothing it shows changed. - **Logic hides in templates and components**, where it can only be tested by rendering them. ## What `createSelector` adds The NgRx docs list five properties of selectors: portability, memoization, composition, testability and type safety. In practice: 1. **One definition.** `selectOutstandingTotal` lives in `invoices.selectors.ts`; every reader imports it. 2. **Memoization.** A selector made by `createSelector` or `createFeatureSelector` keeps its **last arguments and last result**. Called again with the same root state it returns the cached value at once; called with a new root state it reruns its inputs, and it reruns the projector only if at least one input result changed by reference (`===`). Reducers that do not handle an action return their slice unchanged, so an action about the user menu leaves the invoices array identical and the total is not recalculated. 3. **Composition.** Selectors are valid inputs to other selectors, so `selectCustomerView` is built from `selectInvoices` and `selectSelectedCustomerId` rather than from raw state. 4. **Testability.** A pure function of its inputs can be called with a hand-made state object and checked. 5. **Type inference.** The selector carries its state and result types, so a component can inject `Store` without a generic and still get a typed result. ## Reading a selector from a component | Read | Returns | Suppresses repeats with | Typical consumer | |---|---|---|---| | `store.select(selectOutstandingTotal)` | `Observable<number>` | `distinctUntilChanged()` (strict `===`) | `async` pipe, RxJS pipelines | | `store.selectSignal(selectOutstandingTotal)` | `Signal<number>` | the signal's equality (`Object.is`, or an `equal` option) | templates, `computed()` | Both accept any `(state) => value` function, so a plain arrow function still works. What it loses is the cache and the reuse. The NgRx ESLint plugin's `store` config turns on `prefer-selector-in-select`, which flags `this.store.select((state) => state.customers)` and string keys, and `avoid-mapping-selectors`, which flags `store.select(selector).pipe(map(...))` because the mapping belongs inside a selector. ```ts import { createFeatureSelector, createSelector } from '@ngrx/store'; export const selectInvoicesState = createFeatureSelector<InvoicesState>('invoices'); export const selectInvoices = createSelector(selectInvoicesState, (s) => s.items); export const selectOutstandingTotal = createSelector(selectInvoices, (invoices) => invoices.filter((i) => i.status !== 'paid').reduce((sum, i) => sum + i.amount, 0) ); ``` ## Where the logic does not belong Selectors are for **reading and deriving**. They do not fetch missing invoices, dispatch actions or write derived values back into the store; loading belongs to effects and writing to reducers. A derived number such as the overdue count is not stored in state at all: it is recomputed from the invoices on demand, and memoization is what makes that cheap. Keeping derived data out of the reducer means it can never drift out of sync with the invoices it was derived from. ## What a healthy selectors file looks like Teams that get the most from selectors keep a few habits: - **Name them `select...`.** The ESLint rule `prefix-selectors-with-select` enforces it, and it makes reads easy to find. - **Keep input selectors trivial.** An input should return something already stored, such as `state.items`; filtering and summing go in projectors, where the cache protects them. - **Build view models in selectors.** A header that needs the total, the overdue count and the selected customer gets one composed selector rather than three reads combined in the component, which `avoid-combining-selectors` encourages. - **Export them from one file per feature**, so a change to the invoices slice has one obvious place to update.
- Why can a component inject NgRx's Store without a state generic when it reads only through createSelector selectors?The selector carries the types. `Store` defaults to `Store<object>`, and `createFeatureSelector<InvoicesState>('invoices')` produces a `MemoizedSelector<object, InvoicesState>`, so everything composed from it fits and the result type is inferred. The generic is still needed with the deprecated string-key form of `select`, where nothing can be inferred.
- Is a plain arrow function passed to store.select a selector, and what does it give up?It works: `select` accepts any `(state) => value` function and still drops strictly equal repeats with `distinctUntilChanged`. But it has no memoization, so a filter or sum inside it reruns on every state emission, and a derived array is a new reference each time, so it emits even when its contents are equal. It also cannot be shared or composed as a cached input.
saying these in an interview costs you the question
- Selectors are only a naming convention; an inline map in the component is equivalent.
- createSelector deep-compares state, so any derived object shape is cached safely.
- Derived totals should be stored in the reducer so selectors just read them.
- A selector may fetch missing invoices from the server when the slice is empty.
- Components should subscribe to the whole store and pick fields in their templates.