skip to content

Why did NgRx deprecate selectors with props in favour of factory selectors, and how would you migrate a customer-invoices selector that takes a customerId prop?

level: seniorimportance: should knowfreq 30%

answer

  1. props flow through every input
  2. one cache, many customers
  3. weak typing, broken child memoization
  4. a function that returns createSelector
  5. one instance per reader

basics

~20 s

Props selectors share one single-entry cache across every caller's props, type poorly and memoize child selectors badly. Replace them with a factory, (customerId) => createSelector(...), one instance per reader. Deprecated since NgRx 12, slated for removal in v23.

solid answer

~50 s

A props selector receives an extra object, `store.select(selectCustomerInvoices, { customerId })`, that NgRx passes to every input selector and, as the last argument, to the projector. Its cache is keyed on the state and the props, and it holds one entry, so a single module-level selector read by widgets showing different customers keeps evicting itself. The v12 migration guide adds that factory selectors are typed and memoize child selectors correctly. Selectors with props were deprecated in NgRx 12; the 22 source still ships them and says they "will be removed in v23". The replacement is a **factory selector**: `const selectCustomerInvoices = (customerId: string) => createSelector(selectInvoices, (invoices) => invoices.filter((i) => i.customerId === customerId))`. Each call returns an independent memoized selector, so a component creates its instance once and keeps it. If the parameter is really application state, such as the selected customer, compose it from a selector instead.

code

ts · 19 lines
ts
import { Component, OnInit, Signal, inject, input } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectCustomerInvoices } from './invoices.selectors';
import { Invoice } from './invoice.model';

@Component({
  selector: 'app-customer-card',
  template: `@for (inv of invoices(); track inv.id) { <p>{{ inv.id }}: {{ inv.amount }}</p> }`,
})
export class CustomerCardComponent implements OnInit {
  private readonly store = inject(Store);
  readonly customerId = input.required<string>();
  protected invoices!: Signal<Invoice[]>;

  ngOnInit() {
    // one selector instance per card, created once, with its own cache
    this.invoices = this.store.selectSignal(selectCustomerInvoices(this.customerId()));
  }
}

go deeper

for a junior

Recall that selectors with props are deprecated and that a factory selector is a function taking the parameter and returning createSelector.

for a middle

Explain how props flow through every input and the projector, and why one shared cache entry thrashes when callers pass different props.

for a senior

Plan the migration before v23: find props reads, move parameters into factories, instantiate once per reader, and decide which parameters really belong in state. Know that 22 still runs props code.

for a principal

Weigh migrating now against waiting for the major that removes them: the cost is small and mechanical, and it also fixes real cache thrashing, so batching it with other upgrade work is usually the cheaper path.

## What a selector with props was Before NgRx 12, the way to parameterise a selector was **props**: an extra argument passed at the read site. ```ts // Deprecated: props flow through every input and into the projector export const selectCustomerInvoices = createSelector( selectInvoices, (invoices, props: { customerId: string }) => invoices.filter((i) => i.customerId === props.customerId) ); // read site this.invoices$ = this.store.select(selectCustomerInvoices, { customerId: 'c-42' }); ``` In the 22 source, when props are present every input selector is called as `input(state, props)` and the projector receives the input results followed by `props`. The selector's outer cache is keyed on both the root state and the props object. ## Why it was deprecated The v12 migration guide lists three benefits of factory selectors, which are the flaws of props: - **Memoization.** One module-level selector has one cache entry. Two widgets passing `c-42` and `c-7` overwrite each other on every read, so the projector runs every time. The docs' own workaround was to wrap the props selector in a factory, which already pointed to the real fix. - **Types.** The guide calls factory selectors typed; with props, every input in the chain must accept the same props shape, and the read site passes an object the selector cannot check on its own. - **Child selectors.** The guide states that with factories "child selectors are correctly memoized", which the props form, pushing one props object through the whole chain, did not guarantee. The newer API points the same way: `store.selectSignal` takes a selector and an options object, with no props parameter at all. ## Status in NgRx 22 | Item | Status at 22.0.1 | |---|---| | `createSelector` props overloads | `@deprecated`, "will be removed in v23" | | `store.select(selector, props)` | `@deprecated`, same message | | `select` operator with props | `@deprecated`, same message | | Factory selectors | the documented replacement | Deprecated means still working: props code compiles and runs on 22, with editor warnings. ## Migrating to a factory selector 1. Turn the prop into a **function parameter** and drop `props` from the projector. 2. Return `createSelector(...)` from that function, closing over the parameter. 3. At each read site, **create the instance once** and keep it: a field initializer when the parameter is known, `ngOnInit` when it comes from a required input. 4. Replace `store.select(selector, props)` with `store.select(instance)` or `store.selectSignal(instance)`. ```ts export const selectCustomerInvoices = (customerId: string) => createSelector(selectInvoices, (invoices) => invoices.filter((i) => i.customerId === customerId) ); ``` Note the half-step the v12 guide shows as still wrong: a no-argument factory that returns the **same props selector**, `() => createSelector(selectInvoices, (invoices, props) => ...)`. It gives each reader its own cache but keeps props; the migration is complete only when the parameter moves into the factory's arguments. ## Instance lifetime and the parameter that should be state Each factory call builds a new selector with its own cache. Created once per component, the instance lives and dies with that component, and nothing needs releasing. Created on every read, from a template method or getter, it never hits its cache. If the parameter can change, create a new instance when it changes, not per read. Not every parameter needs a factory. The dashboard's **selected customer** is application state, held in the invoices slice as `selectedCustomerId`. A selector composed from `selectInvoices` and `selectSelectedCustomerId` needs no parameter, is shared by every reader, and follows the selection automatically. Reach for a factory when the parameter belongs to one component, such as a card that always shows one customer, or a list rendering several customers side by side. ## Finding what to migrate Editors strike through the deprecated overloads, which makes most sites visible. To be thorough, search for projectors whose last parameter is named `props`, and for `store.select(...)` or `select(...)` calls with a second, object argument. Migrate a selector and all its read sites in one change, since the factory has a different call shape. Avoid caching factory instances in a module-level `Map` keyed by id unless you also remove entries: each instance keeps its last result, so a map that only grows holds every customer's derived list for the life of the app.

  • When is a factory selector the wrong tool for the dashboard's selected-customer view?
    When the parameter is already application state. The dashboard keeps `selectedCustomerId` in the invoices slice, so a selector composed from `selectInvoices` and `selectSelectedCustomerId` needs no parameter, is shared by every reader and updates when the selection changes. A factory fits a parameter owned by one component, such as a card that always shows one customer.
  • Is wrapping the old props selector in a no-argument function a complete migration?
    No. `() => createSelector(selectInvoices, (invoices, props) => ...)` gives each reader its own cache, which fixes the thrashing, but it still reads `props` and still relies on the deprecated overloads. The v12 guide shows it as a before state; the migration ends when `customerId` becomes the factory's parameter and the projector no longer takes props.

saying these in an interview costs you the question

  • Selectors with props were removed in NgRx 15, so props code no longer compiles.
  • Props reach only the projector; the input selectors never see them.
  • A factory selector shares one cache across all the ids it is called with.
  • Wrapping the props selector in a no-argument function completes the migration.
  • selectSignal takes a props object as its second argument, like select did.