NgRx Selector Memoization When Input References Change Each Change Detection Cycle
0 reputation · 31 Jan 2021, 19:12 UTC
0 reputation · 31 Jan 2021, 19:12 UTC
Goal: Understand whether an NgRx selector created with createSelector will recompute on every Angular change detection cycle when its input observable emits a new object reference each cycle, even if the underlying immutable state has not changed.
Constraints: The selector uses the default projection function (reference equality), the application relies on immutable state updates, and the component supplies the selector input via a method that returns a fresh object on each render.
Uncertainty: It is unclear if the selector’s memoization will prevent unnecessary recalculations under these conditions, or if a custom comparator or the immutable option in createFeature is required to stabilize inputs.
Questions: Does the selector recompute on each change detection cycle when the input reference changes but the data is unchanged? Does using the immutable option in createFeature eliminate the recomputation? Would providing a custom comparator to createSelector yield the same effect without additional overhead?
34775 reputation · 01 Feb 2021, 05:07 UTC
Yes. If the selector receives a new object reference on each change detection cycle, the default NgRx memoization will re-run the projection even when the underlying data is deeply equal. The store's immutable state does not help if the changing reference is a component-supplied props object. If the selector's inputs are store slices that did not change, their references stay the same and the projection is skipped.
createSelector memoizes the projector by comparing the input results with reference equality (===). When a component method returns a fresh object each render and that object is passed as props to a parameterized selector, the selector sees a new input reference. The memoization check fails, so the projector runs again. The projected value may still be equal, but the work has already been done.
Recomputation and emission are separate. Even if the projector returns the same reference and store.select suppresses an emission with distinctUntilChanged, the projector still executed. That is the cost you are trying to avoid.
createFeature's immutable option fix it?No. There is no immutable option on createFeature that changes selector memoization. Immutability is a developer contract enforced by your reducers and dev-mode checks; it does not stabilize references for selectors. If you saw an immutable flag elsewhere, it is not a runtime switch for this problem.
It can. Depending on your NgRx version, createSelector may accept a memoization config with a custom equality check. A comparator that treats two props objects as equal when their contents match will prevent the projector from re-running. But it is not free:
So a custom comparator is not "the same effect without overhead." It trades reference stability for comparison cost. Use it only when you cannot stabilize the reference at the component level.
The root cause is the component pattern, not the selector. Move the object creation out of the template or method that runs each change detection cycle.
ngOnChanges or from an observable with distinctUntilChanged.Example of a stable reference:
// Instead of: this.store.select(selectThing, this.getProps())
// where getProps() returns { id: this.id } on every call:
private readonly props = { id: 'fixed-id' };
// Or derive once when the input changes:
ngOnChanges(): void {
this.props = { id: this.id };
}
If the props must change, make the change explicit and reference-stable. Then the selector will skip the projector when the store slice and props are unchanged.
console.log inside the selector projector.Object.is(previousProps, nextProps) in the component or in a custom memoize function.If the counter still increases with a stable props reference, check whether the selector is being re-created (for example, inside a component method) or whether an input selector is returning a new reference.
Which NgRx version are you using? The core reference-equality behavior is stable, but the shape of the custom memoization config for createSelector differs across versions. Check your installed version's createSelector overloads before adding a comparator.
Assumption: NgRx v13+ and Angular's default change detection. Verify the memoization config API against your installed version.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 31 Jan 2021, 20:01 UTC
Even when the feature state is marked immutable, NgRx’s selector memoization only looks at the references of the values returned by its input selectors (or props). If a component supplies a fresh object each change detection cycle, the selector’s cache is cleared and the projector runs, regardless of how the state is updated.
To stop the unnecessary work you need to stabilize that input before it reaches the selector. A common pattern is to memoize the observable stream with distinctUntilChanged (or shareReplay) so that the same reference is emitted until the underlying data actually changes:
const stableSlice$ = this.store.select(state => state.someSlice).pipe(distinctUntilChanged());
this.slice$ = stableSlice$.pipe(map(slice => createSelector(...)(slice)));
This keeps the selector’s input reference stable, allowing its internal memoization to skip recomputation when the slice hasn’t changed.