reactjstypescriptreact-reduxts-jestreselect

Correct use of reselect createSelector and memoizeOptions


I am working on building a custom selector using the createSelector from reselect. The input selectors will be returning data that will fail the default === comparison. To make sure I understood the behavior I am building some fundamental tests that return arrays from the input selectors.

I am expecting the results will be recomputed with every call to the selector, but that is not what I am seeing. The function is not recomputing even when I had memoizeOptions that ensure the input selector comparisons return false.

I am not sure what I am doing wrong or what fundamental mistake I am making in this test that is tripping me up. I would appreciate any help getting this figured out.

I am using Jest v27.4.7, node v15.12.0, with reselect 4.1.6 on Linux.

import {test, expect} from '@jest/globals';
import { createSelector } from "reselect";

test("createSelector:array", async () => {

    const a: number[] = [1, 2, 3, 4, 5];
    const b: string[] = ["a", "b", "c", "d", "e"];
    const state0: { a: number[], b: string[] } = { a, b };

    // https://github.com/reduxjs/reselect#reselect
    //    > createSelector determines if the value returned by an input-selector has changed between calls
    //    > using reference equality (===). Inputs to selectors created with createSelector should be immutable.
    const select = createSelector(
        (state, index: number): number[] => state.a.slice(0, index),
        (state, index: number): string[] => state.b.slice(0, index),
        (a_in: number[], b_in: string[]): (number | string)[] => [...a_in, ...b_in],
        { memoizeOptions: { equalityCheck: (a, b) => { console.debug("equalityCheck", a, b); return false }, resultEqualityCheck: (a, b) => { console.debug("resultEqualityCheck", a, b); return false } } } // THESE DO NOTHING, WHY?
    );

    let out = select(state0, 2);
    expect(out).toEqual([1, 2, "a", "b"]);
    expect(select.recomputations()).toEqual(1);

    out = select(state0, 2);
    expect(out).toEqual([1, 2, "a", "b"]);
    expect(select.recomputations()).toEqual(2); // THIS IS 1, WHY?
});

Solution

  • Per the official reselect documentation:

    If the selector is called again with the same arguments, the previously cached result is returned instead of recalculating a new result.

    So here is how reselect works internally, when you call a selector a second time, reselect will first check to see if the arguments that you passed in to the selector have changed since last time, in your case, state0 and 2. if they have it will then traverse your dependencies (the input selectors), run each one and check to see if their results have changed since last time, and if they haven't, it will skip running your output selector (or resultFunc), and if they have changed it will run your output selector and the recomputations counter gets incremented. So it has 2 layers of checks:

    1. Checks the arguments passed into the selector itself.
    2. Checks the returned value of input selectors.

    In your case the reason why your selector doesn't recompute is that your selector is passing the first check, you're calling the selector itself with the exact same arguments twice. So it doesn’t recompute. You could do something like this:

    let out = select({ ...state0 }, 2);
    

    equalityCheck is used to check the return values of your input selectors with the previous ones, so it is used to do the second layer of checks.

    resultEqualityCheck is used to check the current result of your output selector with the previous one. The best way to test it is to do this:

    let out = select({ ...state0 }, 2);
    const firstResult = select.lastResult();
    expect(out).toEqual([1, 2, "a", "b"]);
    expect(select.recomputations()).toBe(1);
    
    out = select({ ...state0 }, 2);
    const secondResult = select.lastResult();
    expect(firstResult).toBe(secondResult); // If resultEqualityCheck returns true, this should pass.
    

    So the order of execution is:

    I understand your confusion, because in reselect you can’t customize the equality check function for the first layer of checks, reselect uses reference equality checks to see if the arguments passed into the selector itself have changed and as of now does not allow you to customize it. So you can rewrite your tests to look more like this:

    let out = select({ ...state0 }, 2);
    const firstResult = select.lastResult();
    expect(out).toEqual([1, 2, "a", "b"]);
    expect(select.recomputations()).toBe(1);
    
    out = select({ ...state0 }, 2);
    const secondResult = select.lastResult();
    expect(firstResult).toEqual(secondResult);
    expect(out).toEqual([1, 2, "a", "b"]);
    expect(select.recomputations()).toBe(2);