Selectors are methods used for obtaining slices of store state. @ngrx/store provides a few helper functions for optimizing this selection.
When using the createSelector
and createFeatureSelector
functions @ngrx/store keeps track of the latest arguments in which your selector function was invoked. Because selectors are pure functions, the last result can be returned when the arguments match without reinvoking your selector function. This can provide performance benefits, particularly with selectors that perform expensive computation. This practice is known as memoization.
The createSelector
method returns a callback function for selecting a slice of state.
// reducers.ts
import { createSelector } from '@ngrx/store';
export interface FeatureState {
counter: number;
}
export interface AppState {
feature: FeatureState;
}
export const selectFeature = (state: AppState) => state.feature;
export const selectFeatureCount = createSelector(
selectFeature,
(state: FeatureState) => state.counter
);
The createSelector
can be used to select some data from the state based on several slices of the same state.
The createSelector
function can take up to 8 selector function for more complete state selections.
For example, imagine you have a selectedUser
object in the state. You also have an allBooks
array of book objects.
And you want to show all books for the current user.
You can use createSelector
to achieve just that. Your visible books will always be up to date even if you update them in allBooks
. They will always show the books that belong to your user if there is one selected and will show all the books when there is no user selected.
The result will be just some of your state filtered by another section of the state. And it will be always up to date.
// reducers.ts
import { createSelector } from '@ngrx/store';
export interface User {
id: number;
name: string;
}
export interface Book {
id: number;
userId: number;
name: string;
}
export interface AppState {
selectedUser: User;
allBooks: Book[];
}
export const selectUser = (state: AppState) => state.selectedUser;
export const selectAllBooks = (state: AppState) => state.allBooks;
export const selectVisibleBooks = createSelector(
selectUser,
selectAllBooks,
(selectedUser: User, allBooks: Books[]) => {
if (selectedUser && allBooks) {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
} else {
return allBooks;
}
}
);
The createFeatureSelector
is a convenience method for returning a top level feature state. It returns a typed selector function for a feature slice of state.
// reducers.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
export interface FeatureState {
counter: number;
}
export interface AppState {
feature: FeatureState;
}
export const selectFeature = createFeatureSelector<FeatureState>('feature');
export const selectFeatureCount = createSelector(
selectFeature,
(state: FeatureState) => state.counter
);
The selector function returned by calling createSelector
or createFeatureSelector
initially has a memoized value of null
. After a selector is invoked the first time its memoized value is stored in memory. If the selector is subsequently invoked with the same arguments it will return the memoized value. If the selector is then invoked with different arguments it will recompute and update its memoized value. Consider the following:
import { createSelector } from '@ngrx/store';
export interface State {
counter1: number;
counter2: number;
}
export const selectCounter1 = (state: State) => state.counter1;
export const selectCounter2 = (state: State) => state.counter2;
export const selectTotal = createSelector(
selectCounter1,
selectCounter2,
(counter1, counter2) => counter1 + counter2
); // selectTotal has a memoized value of null, because it has not yet been invoked.
let state = { counter1: 3, counter2: 4 };
selectTotal(state); // computes the sum of 3 & 4, returning 7. selectTotal now has a memoized value of 7
selectTotal(state); // does not compute the sum of 3 & 4. selectTotal instead returns the memoized value of 7
state = { ...state, counter2: 5 };
selectTotal(state); // computes the sum of 3 & 5, returning 8. selectTotal now has a memoized value of 8
A selector's memoized value stays in memory indefinitely. If the memoized value is, for example, a large dataset that is no longer needed it's possible to reset the memoized value to null so that the large dataset can be removed from memory. This can be accomplished by invoking the release
method on the selector.
selectTotal(state); // returns the memoized value of 8
selectTotal.release(); // memoized value of selectTotal is now null
Releasing a selector also recursively releases any ancestor selectors. Consider the following:
export interface State {
evenNums: number[];
oddNums: number[];
}
export const selectSumEvenNums = createSelector(
(state: State) => state.evenNums,
evenNums => evenNums.reduce((prev, curr) => prev + curr)
);
export const selectSumOddNums = createSelector(
(state: State) => state.oddNums,
oddNums => oddNums.reduce((prev, curr) => prev + curr)
);
export const selectTotal = createSelector(
selectSumEvenNums,
selectSumOddNums,
(evenSum, oddSum) => evenSum + oddSum
);
selectTotal({
evenNums: [2, 4],
oddNums: [1, 3],
});
/**
* Memoized Values before calling selectTotal.release()
* selectSumEvenNums 6
* selectSumOddNums 4
* selectTotal 10
*/
selectTotal.release();
/**
* Memoized Values after calling selectTotal.release()
* selectSumEvenNums null
* selectSumOddNums null
* selectTotal null
*/
The functions returned by the createSelector
and createFeatureSelector
methods become alternatives to the string syntax for retrieving the relevant piece of state.
// app.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';
import * as fromRoot from './reducers';
@Component({
selector: 'my-app',
template: `
<div>Current Count: {{ counter | async }}</div>
`,
})
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<fromRoot.AppState>) {
this.counter = store.pipe(select(fromRoot.selectFeatureCount));
}
}