In NgRx 22, how do store.select() and store.selectSignal() differ when a component reads an invoice-total selector, and how does each suppress repeat values?
answer
- observable versus signal
- map, then distinctUntilChanged
- computed over the store's state
- Object.is or an equal function
- subscription versus synchronous read
basics
~10 sstore.select returns an Observable built from map plus distinctUntilChanged, so repeats are dropped by ===. store.selectSignal returns a computed signal over the store's state, compared with Object.is or a supplied equal function.
solid answer
~40 sBoth take the same selector; they differ in what they hand back. `store.select(selectOutstandingTotal)` returns an `Observable`: internally it maps each state emission through the selector and pipes the result through `distinctUntilChanged()`, so a value strictly equal to the last one is not emitted. Something must subscribe, usually the `async` pipe. `store.selectSignal(selectOutstandingTotal)` returns a `Signal` built with `computed(() => selector(state()))` over the Store's internal state signal, so it is read synchronously, needs no subscription, and plugs into `computed()` and templates. Its repeat check is the signal's equality, `Object.is` by default, or an `equal` function passed as the second argument. A sensible NgRx 22 default is `selectSignal` for template reads and `select` where the value feeds an RxJS pipeline; either way the selector's own memoization decides whether the projector runs.
code
ts · 18 linesimport { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Store } from '@ngrx/store';
import { selectOutstandingTotal } from './invoices.selectors';
@Component({
selector: 'app-outstanding',
imports: [AsyncPipe],
template: `
<p>Observable: {{ total$ | async }}</p>
<p>Signal: {{ total() }}</p>
`,
})
export class OutstandingComponent {
private readonly store = inject(Store);
readonly total$ = this.store.select(selectOutstandingTotal);
readonly total = this.store.selectSignal(selectOutstandingTotal);
}go deeper
Recall that select returns an observable read with the async pipe and selectSignal returns a signal read by calling it. Both take the same selector.
Explain the implementations, map plus distinctUntilChanged versus computed over the state signal, and the equality each applies. Say why a new array reference passes both checks.
Separate the two layers: the selector's memoization decides whether work runs, the read method's equality decides whether consumers are notified. Use the equal option deliberately, not as a patch for a selector that returns new references.
Set a codebase rule for new components, signals for view reads and observables for stream composition, and weigh the migration cost of existing async-pipe templates against the gain.
## Two ways to read the same selector An NgRx **selector** is a function `(state) => value`, usually built with `createSelector` so it is memoized. The `Store` service offers two methods that turn a selector into something a component can bind to: - **`store.select(selector)`** returns an RxJS `Observable` of the selected value. - **`store.selectSignal(selector, options?)`** returns an Angular `Signal` of the selected value. It has existed since NgRx 16; its options type, `SelectSignalOptions`, has been exported since 21.1. The selector is the same object in both cases, and so is its cache. What differs is how the result is delivered and how repeated values are filtered. ## How `store.select` works In the NgRx 22 source, `select` pipes the store's state stream through `map((state) => selector(state))` and then `distinctUntilChanged()`. So: 1. Every time the store emits a state, the selector is called. A memoized selector usually answers from its cache. 2. `distinctUntilChanged()` drops the value if it is **strictly equal** (`===`) to the previous emission. 3. The result is cold until something subscribes: the `async` pipe, or a manual `subscribe`, which the `no-store-subscription` lint rule discourages because it must be cleaned up by hand. The string-key forms, `store.select('invoices')`, and the props overload are marked deprecated in the 22 source. The select operator form, `store.pipe(select(selector))`, behaves the same as the method; the `select-style` rule picks one spelling for a codebase. ## How `store.selectSignal` works In the same source, `selectSignal` is one line: `computed(() => selector(this.state()), options)`, where `state` is a signal of the root state that the Store maintains. Consequences: - It is **synchronous**: `total()` returns the current value, usable in a template, in another `computed()`, or in an event handler. - There is **no subscription** to clean up; the signal is garbage-collected with whoever holds it. - It is **lazy**: the selector runs when the signal is read after the state has changed, not on every emission. - Its repeat check is the **signal equality**: `Object.is` by default, or the `equal` function passed in options. ```ts readonly customerInvoices = this.store.selectSignal(selectCustomerInvoices, { equal: (a, b) => a.length === b.length && a.every((inv, i) => inv === b[i]), }); ``` ## Side by side | | `store.select` | `store.selectSignal` | |---|---|---| | Returns | `Observable<T>` | `Signal<T>` | | Built from | `map` + `distinctUntilChanged()` | `computed()` over the store's state signal | | Repeat check | `===` | `Object.is`, or `equal` option | | Needs a subscriber | yes (`async` pipe or `subscribe`) | no | | Custom comparison | the selector's memoizer, or your own operators | `equal` option | | Best fit | RxJS pipelines | templates, `computed()` | ## What the repeat check does and does not do Neither method's check changes whether the **projector** runs; that is the memoized selector's business. The checks only decide whether consumers hear about a value: - With `select`, a projector that returns a new array after the invoices change emits, even if the array holds the same invoices, because `===` sees a new reference. - With `selectSignal`, the same new array notifies dependents under `Object.is`; an `equal` function that compares contents keeps the old value and spares templates and `computed()` signals downstream. - A primitive result, such as a total, is filtered correctly by both without help. ## Choosing in NgRx 22 For template bindings, `selectSignal` removes the `async` pipe and subscription management and composes naturally with Angular's signal APIs. `select` remains the right tool when the value is the start of an RxJS pipeline. Neither is deprecated, and both read the same memoized selectors, so the choice is about how the value is consumed, not about caching. ## Where to call it Call either method **once**, where the component is set up: a field initializer, or `ngOnInit` when the selector depends on an input. Calling `store.selectSignal(...)` inside a getter or a template method builds a new computed signal on every read, and calling `store.select(...)` there builds a new observable that the `async` pipe resubscribes to. In both cases the memoized selector underneath still answers from its cache, but the wrapper around it is thrown away each time, so the saving that selectors promise is lost at the last step.
- What does the equal option of selectSignal change, and what does it leave alone?It replaces `Object.is` as the comparison for the computed signal that `selectSignal` builds. When the selector returns a value that `equal` judges equal to the previous one, the signal keeps its old value and its dependents do not re-run. It does not stop the selector from being called after a state change; the selector's own memoization decides whether its projector runs.
- Why might a team keep store.select for some reads after adopting selectSignal?When the value is the start of an RxJS pipeline, for example debouncing a search term held in state before an HTTP call, an observable is the natural input and converting a signal back to a stream adds a step. Both methods read the same memoized selectors, so mixing them costs nothing in caching.
saying these in an interview costs you the question
- selectSignal subscribes to store.select internally, so it must be unsubscribed on destroy.
- store.select emits after every dispatched action, even when the value is identical.
- distinctUntilChanged in store.select compares derived arrays by their contents.
- The equal option of selectSignal stops the selector from being called at all.
- selectSignal accepts a props object as its second argument, just like select.