Go to main content
January 4, 2021
Cover image

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.

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;

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);
  },
);

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?

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

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.

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;
}

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 redux 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)
// 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);

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.

  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.

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);

You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.