Search code examples
reactjsreact-reduxmemoizationreselect

Do separate calls to createSelector with same inputs create multiple memoized results?


When we initially set up our new React project and decided on using Re-Select to memoize our selectors, a pattern was chosen to create wrapper functions around the calls to createSelector() in order to potentially parameterize some of the memoization based on separate component context...

However, over time this has been the exception rather than the rule, and now some of the devs on our team are having a back-and-forth about whether making multiple calls to createSelector with the same inputs results in multiple separate memoized copies of the resulting data, or if they all point to the same result.

Example:

const _getFoos = (state) => state.foos;
const _mapFoosToItems = (foos) => foos.map(createFooItem);

export const selectFooItems = () => {
  return createSelector(_getFoos, _mapFoosToItems);
}

// Component1.tsx
const mapStateToProps = {
 fooItems: selectFooItems()
}

// Component2.tsx
const mapStateToProps = {
 fooItems: selectFooItems()
}

In the above example, are we creating 2 completely separate, memoized selectors with duplicate but separate memoized results? Or does createSelector somehow automagically know that these two selectors are the same because the input selector functions and outputs are the same?

In the example above, since we have no context specific parameters, would it be better to remove the anonymous wrapper around createSelector and just set selectFooItems directly to the createSelector result

Example:

export const selectFooitems = createSelector(_getFoos, _mapFoosToItems);

// Component1.tsx
const mapStateToProps = {
 fooItems: selectFooItems
}

// Component2.tsx
const mapStateToProps = {
 fooItems: selectFooItems
}

Solution

  • Your assumption is accurate.

    The first case, each time selectFooItems is invoked a new selector is created with its own memoization. So even if both are called with identical parameters both will be fully executed because nothing is shared.

    In your second example, since both are sharing the same selector if the second case is called with the same parameters the memoized result will be returned.

    Here is a basic demo of this functionality:

    import { createSelector } from "reselect";
    
    const createFooItem = foo => ({ foo });
    const _getFoos = state => state.foos;
    const _mapFoosToItems = foos => {
      console.log("_mapFoosToItems invoked");
      return foos.map(createFooItem);
    };
    
    export const selectFooItems = () => {
      return createSelector(
        _getFoos,
        _mapFoosToItems
      );
    };
    
    const mapStateToProps1 = {
      fooItems: selectFooItems()
    };
    
    const mapStateToProps2 = {
      fooItems: selectFooItems()
    };
    
    const foos = [1, 2];
    
    // These next two console.log's will produce:
    //  _mapFoosToItems invoked
    //  [{ foo: 1 }, { foo: 2 }]
    //  _mapFoosToItems invoked
    //  [{ foo: 1 }, { foo: 2 }]
    //  Equal: false
    
    const result1 = mapStateToProps1.fooItems({ foos });
    console.log(result1);
    const result2 = mapStateToProps2.fooItems({ foos });
    console.log(result2);
    console.log(`Equal: ${result1 === result2}`);
    
    const shared = createSelector(
      _getFoos,
      _mapFoosToItems
    );
    
    const mapStateToProps3 = {
      fooItems: shared
    };
    
    const mapStateToProps4 = {
      fooItems: shared
    };
    
    // These next two console.log's will produce:
    //  _mapFoosToItems invoked
    //  [{ foo: 1 }, { foo: 2 }]
    //  [{ foo: 1 }, { foo: 2 }]
    //  Equal: true
    
    const result3 = mapStateToProps3.fooItems({ foos });
    console.log(result3);
    const result4 = mapStateToProps4.fooItems({ foos });
    console.log(result4);
    console.log(`Equal: ${result3 === result4}`);
    

    You can also see with the equality check that the memoized result is being returned. Since the _mapFoosToItems method returns the result of .map everytime that method is executed it will return a new array instance. So the only way result3 and result4 could be identical (===) is because the memoized result is returned.