Frontend interview answer

NgRx selectors beyond getting state: memoization, derived state, and Angular performance

HighIntermediateAngular

Interview quick answer

NgRx selectors are memoized projections for derived state; this answer traces four selector boundaries, shows a projector test, and flags the mistakes that cause component churn.

Interview focus

This Angular interview question tests whether you can explain NgRx selector memoization: why projectors rerun, connect it to production trade-offs, and handle common follow-up questions.

  • NgRx selector memoization: why projectors rerun explanation without falling back to memorized definitions
  • Store and Selectors reasoning, edge cases, and production failure modes
  • How you would answer the most likely Angular interview follow-up
Practice more Angular interview questions
Interview answer drill

Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

Full interview answer

Memoized read model

Selectors are not just getters. They are the memoized read layer of an NgRx app: compose feature state into a view model once, reuse the last result while inputs are unchanged, and keep transformation logic out of components. The performance value comes from derived-state reuse, not from magical rerender prevention.

Worked example

Start with a feature selector, then compose small selectors into a final view model like filtered todos plus visible counts. If the input references stay stable, the memoized selector can reuse the last result instead of recalculating the whole projection every time.

Failure pattern

  • If reducers mutate state in place, selector inputs keep the same reference and memoization expectations break.
  • Selectors work best when reducers stay immutable and composition stays small.
  • The goal is reusable derived state, not hiding bad state updates.

Selector level

Example

Responsibility

Feature selector

selectProductsFeature

Grab one feature slice from root state

Entity/base selectors

selectEntities, selectIds

Read normalized raw state

Derived selectors

selectAllProducts, selectFilteredProducts

Transform/filter/sort domain data

VM selector

selectProductsVm

Return final UI-ready shape for component/template

Composed selector flow: feature -> entities -> filtered/sorted VM
TYPESCRIPT
import { createFeatureSelector, createSelector } from '@ngrx/store';

type Product = { id: string; name: string; price: number };
type SortKey = 'name-asc' | 'price-desc';

interface ProductsState {
  entities: Record<string, Product>;
  ids: string[];
  query: string;
  sort: SortKey;
  loading: boolean;
  error: string | null;
}

const selectProductsFeature = createFeatureSelector<ProductsState>('products');

const selectEntities = createSelector(selectProductsFeature, s => s.entities);
const selectIds = createSelector(selectProductsFeature, s => s.ids);
const selectQuery = createSelector(selectProductsFeature, s => s.query);
const selectSort = createSelector(selectProductsFeature, s => s.sort);
const selectLoading = createSelector(selectProductsFeature, s => s.loading);
const selectError = createSelector(selectProductsFeature, s => s.error);

const selectAllProducts = createSelector(selectIds, selectEntities, (ids, entities) =>
  ids.map(id => entities[id])
);

const selectFilteredProducts = createSelector(selectAllProducts, selectQuery, (products, query) => {
  const q = query.trim().toLowerCase();
  if (!q) return products;
  return products.filter(p => p.name.toLowerCase().includes(q));
});

const selectSortedProducts = createSelector(selectFilteredProducts, selectSort, (products, sort) => {
  const copy = [...products];
  if (sort === 'name-asc') return copy.sort((a, b) => a.name.localeCompare(b.name));
  return copy.sort((a, b) => b.price - a.price);
});

export const selectProductsVm = createSelector(
  selectSortedProducts,
  selectLoading,
  selectError,
  (items, loading, error) => ({ items, loading, error, total: items.length })
);
                  

Memoization behavior

What happens

Practical impact

First evaluation

Projector runs and result is cached

Expected initial compute cost

Same input selector references

Cached result returned, projector does not re-run

Avoids unnecessary CPU and re-renders

Any input reference changes

Projector re-runs once and cache is updated

Recompute only when data actually changed

Inputs recreated every time

Memoization is defeated

Performance degrades and UI churn increases

How NgRx selector memoization actually works

Selector purity and projector tests

A selector projector should be a pure function: no services, router calls, dates, random values, in-place mutation, or hidden component state. Test expensive derivation at the projector boundary with selectProductsVm.projector(items, loading, error) so failures stay about transformation logic, not Store setup. The testable proof below is the line between a selector that only sounds memoized and a selector contract a component can trust through store.select(selectProductsVm) or the AsyncPipe.

Testable proof

A focused projector test proves the selector's UI contract without bootstrapping Angular TestBed or the NgRx Store.

TYPESCRIPT
describe('selectProductsVm projector', () => {
  it('builds the component view model from selected inputs', () => {
    const items = [
      { id: 'p1', name: 'Keyboard', price: 80 },
      { id: 'p2', name: 'Monitor', price: 200 }
    ];

    const vm = selectProductsVm.projector(items, false, null);

    expect(vm).toEqual({
      items,
      loading: false,
      error: null,
      total: 2
    });
  });
});
                  

Projector run trace

Use this trace when reviewing whether a selector is actually benefiting from memoization.

Approach

Unrelated update

Projector trace

Review signal

Root-state mapping in component

Root store emits

Runs and rebuilds VM

High churn; logic belongs in selectors

Broad feature selector

Same feature reference changes

Runs even if only a sibling field changed

Split input selectors

Composed atomic selectors

Only unrelated feature changes

Does not run

Good memoization boundary

VM selector over focused inputs

Only selected inputs change

Runs once and returns UI-ready shape

Best component contract

Projector run trace: how selector boundaries affect recomputation
TYPESCRIPT
// ❌ Pitfall: selecting whole state and rebuilding arrays/objects in component
@Component({ selector: 'app-products', template: `...` })
export class ProductsComponent {
  // broad selection + local mapping each emission
  readonly vm$ = this.store.select(state => state).pipe(
    map(state => {
      const items = Object.values(state.products.entities)
        .filter(p => p.name.includes(state.products.query))
        .sort((a, b) => a.name.localeCompare(b.name));

      return {
        items: [...items], // new object/array every time
        total: items.length,
        loading: state.products.loading
      };
    })
  );
}

// ✅ Better: keep composition in selectors, component only selects VM
@Component({ selector: 'app-products', template: `...` })
export class ProductsComponent {
  readonly vm$ = this.store.select(selectProductsVm);
}
                  

Common pitfall

Why it hurts

Fix

Selecting the whole root state in components

Any unrelated state change can trigger unnecessary recalculation

Select the narrowest feature/VM selector

Doing filter/sort/map in components repeatedly

Logic duplication + unstable references + harder tests

Move derivation into composed selectors

Returning brand-new objects everywhere without need

Memoization cannot help if inputs are constantly rebuilt

Preserve stable references where possible and compose selectors

Keeping selectors too shallow (no VM selector)

Components become bloated orchestration layers

Create a single UI-focused VM selector

Selector performance pitfalls senior reviewers spot

Selector factory boundary

Use a selector factory such as selectProductById(id) when a component needs one entity keyed by a route param or input id. Do not create a new factory selector on every change detection pass; create it once for a stable id, then select it. Prefer this boundary over older props-style selectors because each factory instance owns its memoized arguments.

Selector review checklist

  • Is each input selector as narrow as possible?
  • Is derived data kept out of Store state?
  • Is the projector pure and free of services, time, random values, and mutation?
  • Does the component avoid local filter/sort/map after store.select?
  • Is any selector factory created only for a stable parameter?
  • Does at least one projector test cover the expensive derivation?

FrontendAtlas review note

When we review an NgRx selector answer, we look for a concrete boundary: reducers preserve immutable references, input selectors stay narrow, derived state stays out of the Store, and the component receives a final VM selector instead of rebuilding arrays locally. A strong answer also names the failure mode: broad root-state selection and component-local filter/sort/map make memoization look present while projector work still churns.

Source check

Compare this answer with the NgRx selectors guide and createSelector API page. FrontendAtlas content is maintained under the Editorial Policy, with corrections handled through the page issue flow.

Interview summary

NgRx selectors are a memoized read-model layer, not just property accessors. Compose selectors from feature state to entities to derived filtered/sorted data, then expose a final view model selector to components. This reduces component complexity and keeps state logic reusable.

Selector memoization trace

Input boundarySelected state

Component selects the whole root store.

ProjectorRuns

Mapping work runs again because every root emission reaches the component.

Component VMRebuilt

The component rebuilds a new array/object VM.

Unrelated update

A notification badge updates outside the products feature.

Review signal

High churn: move derivation into composed selectors.

Similar questions
Guides
Preparing for interviews?