When do selectors recalculate? #4267
-
Hi all, Given that (provided I understand the docs correctly) we should create deep copies of the state,
and given that selectors recalculate when their inputs change,
I was wondering how selectors determine if their input changed - or, put differently: when do they recalculate? Do they run a deep compare, or do they somehow check by reference? Cheers, |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
The inputs for selectors are determined when the state changes. The inputs are compared by reference for objects, and by value for primitives. Selectors don't have to be recalculated if selectors are composed in small slices that can be memoized. By looking at a simplified implementation of how
function createSelector(...otherSelectors, projector) {
// Define an internal state for memoization
let lastMemoizedArguments: Array<unknown> = undefined;
let lastMemoizedResult: unknown = undefined;
// Return a function that, for the current state, returns either a memoized value or a new value
return (state) => {
// Apply the dependent selectors to the state
const otherSelectorResults = otherSelectors.map(selector => selector(state));
// Given the results of the other selectors, check if the memoized value is applicable to the results
const result = lastMemoizedArguments?.every((arg, i) => arg === otherSelectorResults[i])
? lastMemoized.result
: projector(...arguments);
// Memoize the current result
lastMemoizedArguments = otherSelectorResults;
lastMemoizedResult = result;
return result;
};
} So, selectors simply apply a chain of functions to a state object, and the chain will stopped being evaluated and return early when there are memoized results. Then, for You can imagine implementing your own version of class Store<State> {
select(selector: (state: State) => unknown) {
return this.state$.pipe(
map(state => selector(state)),
distinctUntilChanged() // don't emit a new value unless the value changes
);
}
} Hopefully that helps explain how and when selectors are applied. Here's a further example: Say we have the following selectors for finding students who have a passing grade on a test: // Example of a manually create state selector
const selectState = createSelector(
(state: State) => state,
state => state
);
const selectStudents = createSelector(
selectState,
state => state.students
);
const selectTestGradesByStudentId = createSelector(
selectState,
state => state.testGradesToStudentId
);
const selectPassedTestsByStudentId = createSelector(
selectTestGradesToStudentId,
testGradesToStudentId => Object.fromEntries(
Object.entries(testGradesToStudentId).filter(([_, { grade }]) => grade > 60)
)
);
const selectStudentsWhoPassedTest = createSelector(
selectPassedTestsByStudentId,
selectStudents,
(passedTestsByStudentId, students) => students.filter(student => !!passedTestsByStudentId[student.id])
); Then when we are subscribed to this selector: const studentsWhoPassedTest$ = this.store.select(selectStudentsWhoPassedTest);
studentsWhoPassedTest$.subscribe(); Then say the current state is: {
students: [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}],
testGradesToStudentId: {'1': {grade: 65}, '2': {grade: 55}}
} All the selectors would be called immediately, with their arguments memoized. If we produce a new state object where we add another student, but not another test grade, then the selectors would be evaluated like:
|
Beta Was this translation helpful? Give feedback.
-
Thanks! So (if I wanted to change an existing student, not add a new one - for example, change
they'd be referring to the individual student in your example (if they were complex and not flat objects), not the entire state : One more question regarding your example, though: I guess it depends on whether your reducer would |
Beta Was this translation helpful? Give feedback.
The inputs for selectors are determined when the state changes. The inputs are compared by reference for objects, and by value for primitives.
Selectors don't have to be recalculated if selectors are composed in small slices that can be memoized.
By looking at a simplified implementation of how
createSelector
andstore.select
work, then you'll see how selectors get updates.createSelector
is a function that simply creates another function that takes in state and returns a value. Here's a simplified example: