Reselect: Checking equality on only 1 parameter of a multi parameter selector

Created on 12 May 2016  路  5Comments  路  Source: reduxjs/reselect

Hi there,

Is it possible to stop a recomputation if one of the parameters (e.g. the first parameter) of a selector returns true on the prev == next equality check? and thus return the previously cached value?

I am using normalizr to produce a state that looks like so

state.images
state.imageGalleries

an imageGallery has a list of images' ids (i.e. ImageGallery.images)

I want to create a selector that returns a list of filtered images from state.images for a given imageGallery.

the selector I currently have is something like this:

createSelector(
    getListOfGalleryImageIds, getAllImagesFromState,
    (galleryImageIds, allImages) => {

      // Pick out Images from the state that are listed in 'galleryImageIds'
      return filteredImages;
    }
  );

The issue I am having is many of my react 'imageGallery components' share the state.images state. If a new imageGallery is created and I fetch additional images for this gallery to merge into my state.images then it would trigger reselection for all of my components? Is it possible to stop re-computation if my first parameter 'getListOfImageIds' does not change and return the previously cached value instead? Or do you think this is more of an issue with the design of my state structure?

Thanks for your help

Most helpful comment

Hey I packaged these ideas up with tests here:

https://www.npmjs.com/package/reselect-equality-check-n-parameters

All 5 comments

I like your idea, @ericj17. We can solve it in the same fashion as memoizee does, with an additional length argument, meaning that first length arguments will be cached and everything else will be ignored (but still passed down to the original function). One possible implementation:

export function defaultMemoize(func, length, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null;
  let lastResult = null;
  return (...args) => {
    const slicedArgs = args.slice(0, length === undefined ? args.length : length);

    if (
      lastArgs !== null &&
      lastArgs.length === slicedArgs.length &&
      slicedArgs.every((value, index) => equalityCheck(value, lastArgs[index]))
    ) {
      return lastResult
    }
    lastArgs = slicedArgs;
    lastResult = func(...args);
    return lastResult
  }
}

@ellbee, would you like to have this functionality out of the box? It shouldn't impact performance in any significant way, because it's only one additional slice call (with an extra if it could be called only when the length argument is not undefined).

Probably not out of the box for the moment as I am trying to keep the API as simple (the README is already very long) and stable (Reselect has a lot of users now) as possible, but I like the idea.

I would like to be a bit more liberal in accepting convenience functions for less common use cases though. My concerns are:

  • 100% backwards compatibility.
  • Majority of users just need createSelector, so they shouldn't have to import convenience functions they don't need.

Maybe being able to import separate functions like lodash/redux does might enable this.

@Dattaya @ellbee Thank you both for your thoughts.

I initially wrote a static custom memoizer that only checks the first argument. But I have ended up using a wrapper function around 'createSelectorCreator', below, to accept a parameter for how many arguments to check (using Dattaya's memoizer above, thanks!):

const createFirstNArgsSelector = function(nArgsToCheck) {

  const defaultEqualityCheck = function (a, b) {
    return a === b;
  }

  const memoizer = function (func, equalityCheck = equalityChecker) {
    let lastArgs = null;
    let lastResult = null;

    return (...args) => {
      const slicedArgs = args.slice(0, nArgsToCheck === undefined ? args.length : nArgsToCheck);

      if (lastArgs !== null
        && lastArgs.length === slicedArgs.length
        && slicedArgs.every((value, index) => equalityCheck(value, lastArgs[index]))
      ) {
        return lastResult;
      }
      lastArgs = slicedArgs;
      lastResult = func(...args);
      return lastResult;
    }
  };

  return createSelectorCreator(
    memoizer
  )
};

and using it in the following way e.g.:

export const makeGetListOfGalleryImages = () => {
  const checkFirstArgOnlySelector = createFirstNArgsSelector(1);
  return checkFirstArgOnlySelector( // Check only the first argument for equality
    getListOfGalleryImageIds, getAllImagesFromState,
    (galleryImageIds, allImages) => {

      // Pick out Images from the state that are listed in 'galleryImageIds'

      return filteredImages;
    }
  );
};

Can the above code be further optimized? Welcome your feedback.
P.s. I haven't tested the exact code above as I am using a typescript'd version in our project.

Thanks for your support :)

Probably not out of the box for the moment as I am trying to keep the API as simple (the README is already very long) and stable (Reselect has a lot of users now) as possible, but I like the idea.

@ellbee, please, let me know if you do decide to ship this functionality with reselect and in what form, e.g.createSelector could take optional options object as first/last argument (seems like it's not a breaking change), I will be happy to contribute.

Can the above code be further optimized? Welcome your feedback.'

Definitely, If all you need is to memoize only one argument, you can get rid of slice and every:
(not tested):

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArg = null;
  let lastResult = null;
  return (...args) => {
    if (
      lastArg !== null &&
      equalityCheck(lastArg, args[0])
    ) {
      return lastResult
    }
    lastArg = args[0];
    lastResult = func(...args);
    return lastResult
  }
}

Hey I packaged these ideas up with tests here:

https://www.npmjs.com/package/reselect-equality-check-n-parameters

Was this page helpful?
0 / 5 - 0 ratings