How to nest Reselect selectors?

1.7k views Asked by At

I have a selector that returns an array. The elements in the array themselves have derived data. I essentially need a recursive memoization selector that returns a derived array composed of derived elements.

my current attempt is:

export const selectEntitesWithAssetBuffers = createSelector(
  [selectSceneEntities, getAssets],
  (entities, loadedAssets) => {
    return entities.map((entity) => {
      entity.buffers = entity.assets.map((assetName) => {
        return loadedAssets[assetName].arrayBuffer;
      })
      return entity;
    })
  }
)

My concerns here are anytime entities or loadedAssets change this will recompute the entire list. What I'm expecting to setup is something like a selectEntityWithBuffer that would get passed to the entities.map. Ideally, I want this to only recompute when an entity.assets array changes.

3

There are 3 answers

0
Jake Haller-Roby On BEST ANSWER

Reselect allows you to provide custom equality definitions to your selectors.

import { defaultMemoize, createSelectorCreator } from 'reselect'

const compareByAssets = (a, b) => {
    return a.every((element, index) => {
        return element.assets === b[index].assets
    });
};

const createAssetsComparatorSelector = createSelectorCreator(
    defaultMemoize,
    compareByAssets
);

const selectSceneEntitiesByAssetsComparator = createAssetsComparatorSelector((state) => {
    //however you normally get entities for the pre-existing selectors
});

Now you can use this new selectSceneEntitiesByAssetsComparator in place of the previous selectSceneEntities in the above code you provided and it will only re-run when the equality check in compareByAssets fails.

Feel free to further update that comparator function if a strict comparison of assets === assets doesn't suite your needs.

0
Rob Wise On

Getting deeper memoization than what you've got is kind of a tricky problem because Reselect doesn't really support passing arguments to selectors. If you're returning an array from your selector, and the input used to build that array has changed, it's sort of the intended behavior from Reselect that you will need to recompute. See the advice in the readme for dynamic arguments.

0
Andrea Carraro On

As a proof of concept, I'd try to provide loadedAssets object to the result function by bypassing reselect identity checks.

// Keep a private selector instance
let cachedSelector;

export const selectEntitesWithAssetBuffers = function(){
    // loadedAssets should be recalculated on each call?
    const loadedAssets = getAssets(arguments);

    // create selector on first call
    if(cachedSelector === undefined) {
        cachedSelector = createSelector(
            selectSceneEntities,
            entities => {
                return entities.map(entity => {
                    entity.buffers = entity.assets.map((assetName) => {
                        return loadedAssets[assetName].arrayBuffer;
                    })
                    return entity;
                })
            }
        )
    }

    // Return selector result
    return cachedSelector(arguments);
}