Memoization is a way to speed up functions by caching their results.
If you call a function with the same inputs, instead of recomputing everything again, it just returns the previous result from memory.
Very simplified:
function memoize<T extends (...args: any[]) => any>(fn: T): T {
let lastArgs: any[] | null = null;
let lastResult: any;
return function (...args: any[]) {
if (
lastArgs &&
args.length === lastArgs.length &&
args.every((arg, i) => arg === lastArgs[i])
) {
return lastResult; // same inputs -> reuse result
}
lastArgs = args;
lastResult = fn(...args);
return lastResult;
} as T;
}
So:
Same args → skip work → return cached result.
Why memoization matters in NgRx
In NgRx you often have:
- A big state tree
- Selectors that derive data from that state
- Components that subscribe to those selectors
Some derived data can be expensive to compute:
- Filtering large lists
- Sorting
- Computing aggregates
- Mapping to complex view models
If your selector recalculates this on every change detection, your app feels heavier than it should.
This is where NgRx comes in with a gift:
Selectors created with
createSelectorare memoized by default.
They only recompute when their inputs change (by reference).
Basic NgRx selector example (with memoization)
Imagine a simple state:
export interface Todo {
id: string;
title: string;
completed: boolean;
}
export interface TodosState {
todos: Todo[];
filter: 'all' | 'completed' | 'active';
}
Feature selector:
export const selectTodosState = createFeatureSelector<TodosState>('todos');
Now a few selectors:
export const selectTodos = createSelector(
selectTodosState,
(state) => state.todos
);
export const selectFilter = createSelector(
selectTodosState,
(state) => state.filter
);
export const selectFilteredTodos = createSelector(
selectTodos,
selectFilter,
(todos, filter) => {
console.log('Recomputing filtered todos...');
if (filter === 'completed') {
return todos.filter((t) => t.completed);
}
if (filter === 'active') {
return todos.filter((t) => !t.completed);
}
return todos;
}
);
What NgRx does for you:
-
selectFilteredTodosis memoized -
It only recalculates when:
-
selectTodosreturns a new array reference, or -
selectFilterreturns a new filter value
-
If unrelated parts of the store change, and the todos array and filter are the same references, NgRx just returns the cached filtered list.
Less CPU, fewer unnecessary component updates.
How this helps your Angular components
Component:
@Component({
selector: 'app-todos',
template: `
<div *ngFor="let todo of todos$ | async">
{{ todo.title }}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodosComponent {
todos$ = this.store.select(selectFilteredTodos);
constructor(private store: Store) {}
}
Because selectFilteredTodos is memoized:
-
If state changes somewhere else (e.g. user profile), but todos slice is unchanged:
- Selector returns the same array reference
- With OnPush, Angular sees no new value → no re-render of this list
This is exactly the combo you want:
- NgRx memoization
- OnPush change detection
- Less work, smoother app
Memoization and selectors with props
You can also use memoization when your selector depends on an argument, for example selecting a single todo by id.
export const selectTodoById = (id: string) =>
createSelector(
selectTodos,
(todos) => {
console.log('Recomputing todo by id...', id);
return todos.find((t) => t.id === id) ?? null;
}
);
Then in your component:
todo$ = this.store.select(selectTodoById(this.todoId));
NgRx memoizes per selector instance here:
- For this particular
id, as long asselectTodosreturns the same array reference, the selector returns the cached todo and doesn’t runfindagain.
A subtle gotcha: new objects break memoization
Memoization in NgRx works based on referential equality of the selector inputs.
If you do this:
export const selectTodoViewModels = createSelector(
selectTodos,
(todos) =>
todos.map((t) => ({
...t,
label: `${t.completed ? '✅' : '⬜'} ${t.title}`,
}))
);
Each time selectTodoViewModels recomputes, it returns a new array with new objects.
The selector itself is still memoized, but if todos changes reference often, you’re rebuilding everything.
Better pattern in many cases:
Keep heavy transformations inside selectors, but don’t unnecessarily create new objects if inputs haven’t changed. Memoization helps at the selector level, but your code still needs to be reasonable.
Custom memoization (if you ever need more control)
NgRx also lets you define selectors with custom memoization via createSelectorFactory, but honestly in most apps:
-
createSelector’s built-in memoization is enough - You rarely need to touch lower-level memoization
If you ever have a crazy use case with complex arguments, that’s when you look into selector factories.
Quick recap
- Memoization = caching function results based on inputs
- NgRx selectors created with
createSelectorare memoized by default - They recompute only when their input selectors’ outputs change by reference
- This works beautifully with OnPush and large apps where you can’t afford to recompute everything on every change detection
-
Use selectors to:
- Filter, sort, and derive data
- Keep components lean
- Let memoization + change detection handle performance
Top comments (0)