Here's how NgRx selectors actually work internally
Tags: ngrx
Published: 2024-03-23 03:06:51

## Everyone wants to know "how do selectors really work?"

I ran into [this question](https://github.com/ngrx/platform/discussions/4267) question on the NgRx GitHub discussion threads which asked:

> I was wondering how selectors determine if their input changed - or, put differently: when do they recalculate?

I think every developer that uses NgRx should know the answer to this from the angle of how it really works under-the-hood. Knowing builds your confidence to build performant apps without mystery.

## The fastest answer

When you subscribe to a selector with `store.select`, that selector, and every dependent selector it has, **is called on every single state change**.

That may sound terrible for performance! But, what you may not know is that **selectors are actually composed of two parts**, and the part that is expensive to run on every state change, the "projector," is actually **not** run on every state change.

Selectors only re-calculate, or call their projector again, when their "arguments" change. Let's see what that means.

## What selectors really are

Selectors are composed of a `projector` function and a list of selectors that create "arguments" for the projector when given a state object.

Selectors are declared with `createSelector` like:

```ts
const selectSomething = createSelector(
   ...aListOfSelectors,
   projector
);
```

The projector function **is not called on every state change**.

Instead, when a selector is called:
- each selector in the list of dependent selectors is applied on the state, producing arguments with which to call the projector
- the arguments are compared to a cached, or memoized, list of values with which the selector's projector was last called
- When the arguments are the same, compared by `===`, the projector is not called; the last memoized value is returned instead
- When any of the arguments are different, the projector is called, and the new value is returned; the arguments are memoized along with the new value

## What does this mean?

Selectors don't have to have to be re-calculated, or apply their projector function, on every state change. This is enabled by writing small, composable selectors.

## Break it down, how do selectors achieve memoization?

I'll show a simplified version of how `createSelector` and `store.select` work.

By looking at simplified implementations, hopefully you'll see how the awesome performance benefits of selectors are done.

### How `createSelector` really works

`createSelector` is a factory that creates a function which takes in state and returns a value. The steps it takes are:

- Declare variables for the last arguments with which the projector was called, and the last result of the projector
- Create a function which
  - Applies dependent selectors to the state
  - Checks whether the last arguments with which the projector was called match the results from the dependent selectors
  - Returns either memoized results when true, or applies the projector to the results from the dependent selectors

You could implement `createSelector` like:

```ts
function createSelector(...otherSelectors, projector) {
  // Declare variables for the last arguments which the projector was called with, and the last result of the projector
  let lastMemoizedArguments: Array<unknown> = undefined;
  let lastMemoizedResult: unknown = undefined;

  // Create a function which
  return (state) => {
    // Applies dependent selectors to the state
    const otherSelectorResults = otherSelectors.map(selector => selector(state));

    // Checks whether the last arguments with which the projector was called match the results from the dependent selectors
    const result = lastMemoizedArguments?.every((arg, i) => arg === otherSelectorResults[i])
        // Returns memoized results when true
        ? lastMemoizedResult
        // or applies the projector to the results from the dependent selectors
        : projector(...arguments);
    
    lastMemoizedArguments = otherSelectorResults;
    lastMemoizedResult = result;

    return result;
  };
}
```

### How `store.select` really works

`store.select` creates an Observable based on the state and a selector.

For every single state change, the provided selector is applied to the value. Duplicate results are simply ignored.

You could implement `select` within a global store like:

```ts
class Store<State> {
  select(selector: (state: State) => unknown) {
    return this.state$.pipe(
      map(state => selector(state)),
      // don't emit a new value unless the value changes
      distinctUntilChanged() 
    );
  }
}
```

### So those implementations mean...

`createSelector` handles setting up functions that process state and memoize their results based on child selectors. `store.select` applies a selector function to an Observable of every state change, and only emits changed values.

### Applying what we learned

Let's look at how some real selectors use memoization to avoid unnecessary re-calculations.

Say we have the following selectors for finding students who have a passing grade on a test:

```ts
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:
```ts
const studentsWhoPassedTest$ = this.store.select(selectStudentsWhoPassedTest);
studentsWhoPassedTest$.subscribe();
```

Say the current state was:

```ts
{
  students: [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}],
  testGradesToStudentId: {'1': {grade: 65}, '2': {grade: 55}}
}
```

When we subscribe, `selectStudentsWhoPassedTest` would be called immediately, and all the dependent selectors would also be called, and every projector would be run.

If we update the state by adding another student without adding another test grade, we should expect only the selectors relevant to the students' list to call their projectors:

- When `selectStudentsWhoPassedTest` is called,
  - First, it evaluates its dependent selectors against the current state.
    - `selectPassedTestsByStudentId` calls its dependent selector
      - `selectTestGradesToStudentId` calls `selectState`, which returns the current state object.
        - The projector function is then applied, returning the `testGradesToStudentId` array.
      - Next, the projector function for `selectPassedTestsByStudentId` is applied, using the argument from `selectTestGradesToStudentId`. Since the result is identical to the previous state (same reference), the memoized projector result is returned.
    - Separately, `selectStudents` calls `selectState`, fetching the new state object.
      - Its projector function returns the `state.students` array, now with a new reference due to the updated list of students.
  - Finally, `selectStudentsWhoPassedTest` compares its arguments: the unchanged `passedTestsByStudentId` and the updated array of students. Given the change in one argument, the projector is re-applied.

So when we changed the list of students:
- `selectPassedTestsByStudentId`'s projector didn't have to get called again
- `selectStudentsWhoPassedTest`'s projector got called again using the new list of students

### Wrapping up

In this article you learned:

- Selectors that are part of a subscription are called on every state change
- The projectors of selectors are only called when the results of their dependent selectors change from the previous state value to the current one
- Selector memoization is set up in `createSelector`
- Change notifications of selector values are set up in `store.select`
- Selectors have to be written intentionally to take advantage of their memoization

Without the mystery of how selectors work, you should be able to write performant, composable selectors.