Skip to content

Rainbow
Apps

Rainbow
Apps

How does Reselect work?

Let's focus on the library Reselect, which handles memoization of Redux (and others) selectors. It can be useful when we want to do some selectors with business logic and costly to execute. Or when you want to keep a same reference (when no needs to reprocess) not to re-render when we use PureComponent or memoized component with React.memo.

When we use the High Order Component connect of react-redux, it memoizes our component by default avoiding useless re-rerenders.

Example of usage

Basic selector

Let's take the example of a Redux state where we keep the user session:

const reduxState = {
    user: {
        id: 1,
        firstName: 'Bob',
        lastName: 'Sponge',
        email: 'bob.sponge@gmail.com'
    }
}

If I want to select the user to display its informations in a dedicated component named User:

import { selectUser } from './userSelector';
import { useSelector } from 'react-redux';

function User() {
    // I have written the lambda to see that useSelector give us the state
    // Otherwise we could only write useSelector(selectorUser)
    const user = useSelector(state => selectUser(state));

    return (
        <div>
            <div>First name: ${user.firstName}</div>
            <div>Last name: ${user.lastName}</div>
        </div>
    )
}

export default User;

The selector selectUser is:

export const selectUser = state => state.user;

In this case we only get our data directly from the state without having to process it, either keep a stable reference (because the state should be immutable: if the user change the reference will be too) so we don't have to use reselect.

Selector with reselect

Let's imagine we want to implement a books management site, where we have a slice of the state which handles autors and another one with books:

const reduxState = {
    authors: [
        {
            id: 1,
            firstName: 'John',
            lastName: 'Green'
        },
        {
            id: 2,
            firstName: 'Lauren',
            lastName: 'Weisberger'
        }
    ],
    books: [
        {
            id: 1,
            title: "The devil wears prada",
            authorId: 2
        },
        {
            id: 2,
            title: 'The fault in our stars',
            authorId: 1
        }
    ]
}

If I want to get the list of books with the author with the data structure:

const books = [
    {
        id: Number,
        author: String,
        title: String
    },
    ...
]

It can be useful to do a memoized selector with reselect. Reselect exports a function createSelector which takes as first parameters the dependencies functions et as second (or last see note below) the result function which will return the result after processing. These dependencies functions return objects which are injected as parameters to the result function (in the same order as the defined dependencies functions). Here is an example:

import { createSelector } from 'reselect';

// Should not be mutated
const EMPTY_OBJECT = {};
const EMPTY_ARRAY = [];

const selectAuthors = state => state.authors;

const selectBooks = state => state.books;

// Depends on the selectAuthors function
const selectAuthorsById = createSelector(selectAuthors, authors => {
    return authors.reduce((acc, author) => {
        return (
            {
                ...acc,
                [author.id]: author
            }
        );
    }, EMPTY_OBJECT);
});

function getAuthorName(author) {
    if (!author) {
        return '';
    }

    return author.firstName + ' ' + author.lastName;
}

// Depends on the selectAuthorsById and selectBooks functions
export const selectBooksList = createSelector([selectAuthorsById, selectBooks], 
    (authorsById, books) => {
        return books.reduce((acc, book) => {
            const authorName = getAuthorName(authorsById[book.authorId]);

            return (
                [
                    ...acc,
                    {
                        id: book.id,
                        title: book.title,
                        author: authorName
                    }
                ]
            )
        }, EMPTY_ARRAY);
    });

Note: The dependencies can be passed to the createSelector with 2 ways. The first one, like the example within an Array or the second strategy is as parameters: createSelector(firstDependency, secondDependency, resultCallback)

The result using this selector is:

import { selectBooksList } from './bookSelector';


let reduxState = {
    authors: [
        {
            id: 1,
            firstName: 'John',
            lastName: 'Green'
        },
        {
            id: 2,
            firstName: 'Lauren',
            lastName: 'Weisberger'
        }
    ],
    books: [
        {
            id: 1,
            title: "The devil wears prada",
            authorId: 2
        },
        {
            id: 2,
            title: 'The fault in our stars',
            authorId: 1
        }
    ]
}

console.log(selectBooksList(reduxState));
/*
[
    {
        id: 1,
        title: "The devil wears prada",
        author: 'Lauren Weisberger'
    },
    {
        id: 2,
        title: 'The fault in our stars',
        author: 'John Green'
    }
]
*/

reduxState = {
    ...reduxState,
    otherData: {}
};

// We do not re-execute the selectors selectBooksList and selectAuthorsById.
console.log(selectBooksList(reduxState));

As long as authors and books have the same references, we do not reprocess the result functions of selectBooksList and selectAuthorsById. In our example, we only process the data at the first call of selectBooksList.

But, how does Reselect work under the hood?

Under the hood

Before starting, if you do not feel comfortable with function memoization, I advise you to read the article Javascript memoization.

Basic memoization functions

The first methods of the file index.js:

  • defaultEqualityCheck: function to compare two values with strict equality
  • areArgumentsShallowlyEqual: method to compare with shallow equal, the default comparison method is defaultEqualityCheck but is configurable.
  • defaultMemoize: memoization function of the last value, it takes assecond parameter the comparison method which is by default defaultEqualityCheck

The method defaultMemoize is exported by reselect and can be used in projects using the library.

Main function of reselect

Get dependencies functions

Previously, we have seen that we can pass dependencies with 2 differents ways. To get the right dependencies functions in the 2 cases, we are going to implement a method getDependencies which will take an Array of functions as parameters:

  • either there is a single element and it's an Array of functions
  • or directly an Array of function

This method will return an Array of functions.

function getDependencies(funcs) {
  // If the first element is an Array, we return this one
  // In this case the user has passed directly an Array with the dependencies functions
  // createSelector([firstDependency, secondDependency], resultCallback)

  // Otherwise it's a simple array of functions as elements
  // createSelector(firstDependency, secondDependency, resultCallback)
  const dependencies = Array.isArray(funcs[0]) ? funcs[0] : funcs;

  // We check that all elements are functions otherwise we return an Error
  if (!dependencies.every((dep) => typeof dep === "function")) {
    const dependencyTypes = dependencies.map((dep) => typeof dep).join(", ");
    throw new Error(
      "Selector creators expect all input-selectors to be functions, " +
        `instead received the following types: [${dependencyTypes}]`
    );
  }

  return dependencies;
}

First implementation

From now we know how to get the dependencies functions which will send parameters fo our result function (the last parameter pass to createSelector).

The step will be the following:

  • get the result function (the last parameter in all case)
  • memoizes this method
  • get depedencies functions
  • return a function in which: -- we execute depedencies functions to get an Array of parameters -- we pass these parameters to the memoized function
export function createSelector(...funcs) {
    // We get the result function
    const resultFunc = funcs.pop();
    // We get the dependencies functions Array
    const dependenciesFuncs = getDependencies(funcs);

    // We memoize the result function
    const memoizeResultFunc = defaultMemoize(resultFunc);

    return function() {
        const parameters = dependenciesFuncs.map(func => func(...arguments));

        return memoizeResultFunc(...parameters);
    }
}

This way to code the function createSelector could be possible. However the real implementation is different:

  • the memoization function is configurable
  • the number of times we execute the result function is counted
  • we memoize the returned function not to uselessly re-execute it. For example (with React), while the component using a reselect selector is re-render only because the parent is re-render (no change of redyx state or/and props).
  • the functions are not executed by spreading the parameters but using apply for performances reasons: #194
  • the implementation does not use Array#map also for performance reasons (see the PR above)

arguments is a variable which is accessible in all functions, which helps us to get in an Array all parameters which has been passed to the method.

Improvements

// We can configure the memoization method, and pass option for this one
export function createSelectorCreator(memoize, ...memoizeOptions) {
    return (...funcs) => {
        const resultFunc = funcs.pop();
        const dependenciesFuncs = getDependencies(funcs);
        let recomputations = 0;


        const memoizeResultFunc = memoize(function() {
            recomputations++;

            return resultFunc.apply(null, arguments);
        }, ...memoizeOptions);

        // Optimization not to reprocess when arguments are the same in shallow equals
        // In this case we use the memoization method without options not to change the default behavior
        const selector = memoize(function() {
            // In the current implementation, no use of Array#map for "performances" reasons
            const parameters = []
            const length = dependenciesFuncs.length

            for (let i = 0; i < length; i++) {
                parameters.push(dependenciesFuncs[i].apply(null, arguments))
            }

            return memoizeResultFunc.apply(null, parameters);
        });

        // In the real implementation, we can get the result function 
        // and the dependencies functions from the selector 
        selector.resultFunc = resultFunc;
        selector.dependencies = dependenciesFuncs;

        // Function to get the number of re-reprocess from the selector
        selector.recomputations = () => recomputations;
        selector.resetRecomputations = () => (recomputations = 0);
        return selector;
    }
}

export const createSelector = createSelectorCreator(defaultMemoize);

Performances after improvements

Let's analyze the performances gains of optimization which have been made, for us in 2021?

In all examples, I will use the following closure to measure durations:

function startTimer() {
    const start = process.hrtime();

    // Return the duration in milliseconds
    return function endTimer() {
        const end = process.hrtime(start);

        return end[0] * 1000 + end[1] / 1000000;
    }
}
  1. Array#map vs foreach loop + push

The both implementations I will test are:

const numberElements = [1, 2, 5, 10, 100, 1000];

for (let numberElement of numberElements) {
  const timings = [];
  const array = [];

  // Variable number of elements
  for (let i = 0; i < numberElement; i++) {
    array.push(i);
  }

  // 100_000 iterations
  for (let i = 0; i < 100000; i++) {
    const endTimer = startTimer();

    // Tests performances of Array#map
    array.map((v) => v);

    timings.push(endTimer());
  }

  // We take the average of all iterations
  console.log(numberElement, ' ', timings.reduce((a, b) => a + b) / timings.length);
}

vs

const numberElements = [1, 2, 5, 10, 100, 1000];

for (let numberElement of numberElements) {

  const timings = [];
  const array = [];

  // Variable number of elements
  for (let i = 0; i < numberElement; i++) {
    array.push(i);
  }

  // 100_000 iterations
  for (let i = 0; i < 100000; i++) {
    const endTimer = startTimer();

    // Tests performances of foreact loop + push
    // Named custom in the table below
    const arrayToFill = [];
    for (let j = 0; j < array.length; j++) {
      arrayToFill.push(array[j]);
    }

    timings.push(endTimer());
  }

  // We take the average of all iterations
  console.log(numberElement, ' ', timings.reduce((a, b) => a + b) / timings.length);
}

The performance results after 100_000 iterations are (time in milliseconds):

Number of elementsCustomArray#map
10.00014240.0001189
20.00010980.00009673
50.00010610.00007735
100.00015020.0001135
1000.00059380.0002440
10000.0044210.001771

We can see that Array#map is faster than the custom implementation with foreach loop. So does the PR see above about performances is a fraud? Actually no, we have to go back in the past. The PR has been made in 2016, at this moment it wasn't the same version of V8 Javascript Engine*, the nodejs** version was v6.x.x.

Let's remake with the version 6.17.1 of nodejs:

Number of elementsCustomArray#map
10.00015750.0005169
20.00015520.0007389
50.00026150.001449
100.00037020.002593
1000.0028490.02402
10000.029650.2824

Indeed gains are huge: from 5 to 10 times faster with the custom implementation. Performances have been improved from nodejs v10.x.x, from that version the custom implementation becomes slower than Array#map.

You can find V8 Javascript Engine version corresponding to nodejs versions at this link

  1. Spread operator vs apply
function fakeMethod() {}

const numberElements = [1, 2, 5, 10, 100, 1000];

for (let numberElement of numberElements) {

    const timings = [];
    const array = [];

    // Variable number of elements
    for (let i = 0; i < numberElement; i++) {
        array.push(i);
    }

    // 100_000 iterations
    for (let i = 0; i < 100000; i++) {
        const endTimer = startTimer();

        // We simulate 5 calls to the function by spreading the array
        fakeMethod(...array);
        fakeMethod(...array);
        fakeMethod(...array);
        fakeMethod(...array);
        fakeMethod(...array);

        timings.push(endTimer());
    }

    // We take the average of all iterations
    console.log(numberElement, ' ', timings.reduce((a, b) => a + b) / timings.length);
} 

vs

function fakeMethod() {}

const numberElements = [1, 2, 5, 10, 100, 1000];

for (let numberElement of numberElements) {

  const timings = [];
  const array = [];

  // Variable number of elements
  for (let i = 0; i < numberElement; i++) {
    array.push(i);
  }

  // 100_000 iterations
  for (let i = 0; i < 100000; i++) {
    const endTimer = startTimer();

    // We simulate 5 executions of the method by passing the array to the apply function
    fakeMethod.apply(null, array);
    fakeMethod.apply(null, array);
    fakeMethod.apply(null, array);
    fakeMethod.apply(null, array);
    fakeMethod.apply(null, array);

    timings.push(endTimer());
  }

  // We take the average of all iterations
  console.log(numberElement, ' ', timings.reduce((a, b) => a + b) / timings.length);
}

And I get the following performances (in milliseconds):

Number of parametersSpread operatorapply
10.00017650.0001943
20.00015150.0001637
50.00013980.0001403
100.00017490.0001876
1000.00059810.0006265
10000.0045420.004691

We can see that nowadays with the version of node v15.5.0, the performances with Spread operators are quite better.

Let's test with the version 6.17.1 of nodejs:

Number of parametersSpread operatorapply
10.0013460.0001981
20.0015660.0001617
50.0025250.0002052
100.0038230.0002038
1000.028220.0004328
10000.35170.002866

The optimization is real at the time, almost 10 to 100 times faster! The performances switch from the version 8.x.x of nodejs.

Nowadays possible implementation

We have seen in the previous part that when the PR to improve performances has been created, performances gains was really huged. But nowadays, with the otpimizations made to V8 Javascript Engine, the optimization from the PR are not valid anymore. So we could have the following implementation:

export function createSelectorCreator(memoize, ...memoizeOptions) {
    return (...funcs) => {
        const resultFunc = funcs.pop();
        const dependenciesFuncs = getDependencies(funcs);
        let recomputations = 0;

        const memoizeResultFunc = memoize(function() {
            recomputations++;

            return resultFunc(...arguments);
        }, ...memoizeOptions);

        const selector = memoize(function() {
            const params = dependenciesFuncs.map(dependency => dependency(...arguments));

            return memoizeResultFunc(...parameters);
        });

        selector.resultFunc = resultFunc;
        selector.dependencies = dependenciesFuncs;
        selector.recomputations = () => recomputations;
        selector.resetRecomputations = () => (recomputations = 0);
        return selector;
    }
}

export const createSelector = createSelectorCreator(defaultMemoize);
01-04-2021

Rainbow Apps

Get it on Google Play

© Copyright 2021 Rainbow Apps. All rights reserved.

Rainbow Apps

© Copyright 2021 Rainbow Apps.
All rights reserved.

Get Numbersion app

Get it on Google Play

Follow me