Reselect: Compose selectors with additional arguments

Created on 1 Aug 2015  路  10Comments  路  Source: reduxjs/reselect

Hi there, is it possible to compose selectors with arguments? If have a simple selector which returns a selection, another selector which takes a nested collection (key is passed as a argument) and another selection which consumes the selection:

import createSelector from '../createSelector';

// selection.js: Returns a map of selections: {availability: [], user: []}
export default createSelector(
  [state => state.ui.selection],
  selection => selection
);
import createSelector from '../createSelector';
import selectionSelector from './selection';

// selected.js: Returns a specific selection from the map (e. g. "user")
export default createSelector(
  [selectionSelector],
  (selection, bucket) => selection && selection.hasOwnProperty(bucket) ? selection[bucket] : []
);
import Immutable from 'immutable';
import selectedSelector from '../ui/selected';
import {createImmutableSelector} from '../createSelector';

// selectedUsers.js: Returns a collection of selected users
export default createImmutableSelector(
  [
    state => state.users,
    selectedSelector('user') // This does not work
  ],
  (users, selection) => users.reduce((result, user) => {
    // Select only the users which are selected
  }, Immutable.List())
);

Most helpful comment

Currently you aren't able to parameterise selectors. You could make selectedSelector a factory function that returns a selector:

function selectedSelectorFactory(key) {
  return createSelector(
      [state => state.something],
      something => something[key]
  );
}

All 10 comments

Currently you aren't able to parameterise selectors. You could make selectedSelector a factory function that returns a selector:

function selectedSelectorFactory(key) {
  return createSelector(
      [state => state.something],
      something => something[key]
  );
}

I agree with @ellbee. Using a factory function you could write:

export default createImmutableSelector(
  [
    state => state.users,
    selectedSelectorFactory('user') //Here a new user selector is created on the fly
  ],
  (users, selection) => users.reduce((result, user) => {
    // Select only the users which are selected
  }, Immutable.List())
);

Thank you @ellbee and @speedskater, that solves the problem elegantly.

Great!

This topic came up in Nuclear a while back as I recall. One additional thing to consider is whether the parameter you are passing in is actually transient or if it should actually be state instead. In that case, you could add it to the state tree and make it a normal dependency of the selector.

Apologies for reviving this thread, but can anyone provide insight into how using a factory function effects memoization?

const itemsAll = createSelector(state, (state) => state.items)

vs

const itemById = (id) => createSelector(itemsAll, (items) => find(items, { Id: id }))

I have the same question as @josh-sachs

@madebyherzblut could you please shed some light on @josh-sachs ' s question?

Yes, sure! Since the factory function returns a new function on every call, memoization will not work. But you can just wrap your factory with another memoize function.

The defaultMemoize function from reselect only caches the last result, which may not work for your factory. In the demo I included a memoizer which caches more than one result: Demo

const { createSelector, defaultMemoize } = window.Reselect;

// Source: https://addyosmani.com/blog/faster-javascript-memoization/
function memoize(f) {
  return function() {
    const args = Array.prototype.slice.call(arguments);

    //we've confirmed this isn't really influencing
    //speed positively
    f.memoize = f.memoize || {};

    //this is the section we're interested in
    return args in f.memoize
      ? f.memoize[args]
      : (f.memoize[args] = f.apply(this, args));
  };
}

const shopItemsSelector = state => {
  console.log("shopItemsSelector");
  return state.shop.items;
};

const selectorWithArgWithoutMemoize = expensive => {
  console.log("Create selectorWithArgWithoutMemoize with " + expensive);
  return createSelector(shopItemsSelector, items => {
    console.log("selectorWithArgWithoutMemoize");
    return items
      .filter(item => item.value >= expensive)
      .reduce((acc, item) => acc + item.value, 0);
  });
};

const selectorWithArgWithDefaultMemoize = defaultMemoize(expensive => {
  console.log("Create selectorWithArgWithDefaultMemoize with " + expensive);
  return createSelector(shopItemsSelector, items => {
    console.log("selectorWithArgWithDefaultMemoize");
    return items
      .filter(item => item.value >= expensive)
      .reduce((acc, item) => acc + item.value, 0);
  });
});

const selectorWithArgWithMemoize = memoize(expensive => {
  console.log("Create selectorWithArgWithMemoize with " + expensive);
  return createSelector(shopItemsSelector, items => {
    console.log("selectorWithArgWithMemoize");
    return items
      .filter(item => item.value >= expensive)
      .reduce((acc, item) => acc + item.value, 0);
  });
});

let exampleState = {
  shop: {
    items: [
      { name: "apple", value: 1.2 },
      { name: "peach", value: 5.2 },
      { name: "banana", value: 3.15 },
      { name: "orange", value: 0.95 }
    ]
  }
};

console.log(selectorWithArgWithoutMemoize(3)(exampleState));
console.log(selectorWithArgWithoutMemoize(3.5)(exampleState));

console.log('---');

console.log(selectorWithArgWithDefaultMemoize(3)(exampleState));
console.log(selectorWithArgWithDefaultMemoize(3.5)(exampleState));
console.log(selectorWithArgWithDefaultMemoize(3)(exampleState));
console.log(selectorWithArgWithDefaultMemoize(3.5)(exampleState));

console.log('---');

console.log(selectorWithArgWithMemoize(3)(exampleState));
console.log(selectorWithArgWithMemoize(3.5)(exampleState));
console.log(selectorWithArgWithMemoize(3)(exampleState));
console.log(selectorWithArgWithMemoize(3.5)(exampleState));

The last two calls to selectorWithArgWithMemoize will use the cached result of the previous calls.

This approach works pretty well.

However I am experiencing weird behaviours when passing memoized parameterised selectors to other selectors. For example:

const DEFAULT_USER_ID = 1;

export const getUserById = memoized(id => createSelector(getUser, user => user[id] || emptyMap));

export const getDefaultUser = createSelector(getUserById(DEFAULT_USER_ID), u => u);

screen shot 2018-08-21 at 17 29 27

The id parameter of the memoized function is anything but the id I passed in the other selector.

Does someone knows why? Thanks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vjau picture vjau  路  5Comments

ericj17 picture ericj17  路  5Comments

alvaromb picture alvaromb  路  5Comments

jEnbuska picture jEnbuska  路  6Comments

jwoudenberg picture jwoudenberg  路  3Comments