60

How do I pass additional parameters to combined selectors? I am trying to

• Get data

• Filter data

• Add custom value to my data set / group data by myValue

export const allData = state => state.dataTable
export const filterText = state => state.filter.get('text')

export const selectAllData = createSelector(
  allData,
  (data) => data
)

export const selectAllDataFiltered = createSelector(
  [ selectAllData, filterText ],
  (data, text) => {
    return data.filter(item => {
      return item.name === text
    })
  }
)

export const selectWithValue = createSelector(
  [ selectAllDataFiltered ],
  (data, myValue) => {
    console.log(myValue)
    return data
  }
)

let data = selectWithValue(state, 'myValue')

console.log(myValue) returns undefined

Michal
  • 4,952
  • 8
  • 30
  • 63

7 Answers7

58

Updated: 16 February 2022

New Solution from Reselect 4.1: See detail

// selector.js
const selectItemsByCategory = createSelector(
  [
    // Usual first input - extract value from `state`
    state => state.items,
    // Take the second arg, `category`, and forward to the output selector
    (state, category) => category
  ],
  // Output selector gets (`items, category)` as args
  (items, category) => items.filter(item => item.category === category)
);

// App.js
const items = selectItemsByCategory(state, 'javascript');
// Another way if you're using redux hook:
const items = useSelector(state => selectItemsByCategory(state, 'javascript'));

Updated: 6 March 2021

Solution from Reselect: See detail

// selector.js
import { createSelector } from 'reselect'
import memoize from 'lodash.memoize'

const expensiveSelector = createSelector(
  state => state.items,
  items => memoize(
    minValue => items.filter(item => item.value > minValue)
  )
)

// App.js
const expensiveFilter = expensiveSelector(state)
// Another way if you're using redux:
// const expensiveFilter = useSelector(expensiveSelector)

const slightlyExpensive = expensiveFilter(100)
const veryExpensive = expensiveFilter(1000000)

Old:

This is my approach. Creating a function with parameters and return function of reselect.

export const selectWithValue = (CUSTOM_PARAMETER) => createSelector(
  selectAllDataFiltered,
  (data) => {
    console.log(CUSTOM_PARAMETER)
    return data[CUSTOM_PARAMETER]
  }
)

const data = selectWithValue('myValue')(myState);
Long Nguyen
  • 9,898
  • 5
  • 53
  • 52
  • 4
    would not suggest to use this in a component with changing CUSTOM_PARAMETER; otherwise it will end up creating multiple copies of selectors depending on how it gets used later and this could be extremely expensive since it's memoizing input/output of duplicate and infinite number of permutations that may be ref'd by anything else. (could be good in your selector creators however; just think it's not what the question was) – rob2d Dec 16 '20 at 02:05
  • It is explained here. https://github.com/reduxjs/reselect#q-how-do-i-create-a-selector-that-takes-an-argument – Pranay Kumar Mar 02 '21 at 16:21
  • 3
    your proposed solution it's working but ts it's throwing me this error `Expected 1 arguments, but got 2` any ideas? – Guille Acosta Mar 31 '22 at 23:39
  • I've found the typing solution at https://github.com/reduxjs/reselect/issues/459#issuecomment-758649998 – Guille Acosta Apr 01 '22 at 01:19
16

Here's one with the latest useSelector hook.

The important thing is to get the parameter from the input selector. The input selector's second parameter is how we get it.

Here's how the selector would look,

const selectNumOfTodosWithIsDoneValue = createSelector(
  (state) => state.todos,
  (_, isDone) => isDone, // this is the parameter we need
  (todos, isDone) => todos.filter((todo) => todo.isDone === isDone).length
)

And here's how we extract values with the useSelector hook,

export const TodoCounterForIsDoneValue = ({ isDone }) => {
  const NumOfTodosWithIsDoneValue = useSelector((state) =>
    selectNumOfTodosWithIsDoneValue(state, isDone)
  )

  return <div>{NumOfTodosWithIsDoneValue}</div>
}

Also, keep, the second parameter (isDone) as primitive values (string, number etc.) as much as possible. Because, reselect, only runs the output selector when the input selector value changes. This change is checked via shallow comparison, which will always be false for reference values like Object and Array.

References:

  1. https://react-redux.js.org/next/api/hooks#using-memoizing-selectors
  2. https://flufd.github.io/reselect-with-multiple-parameters/
  3. https://blog.isquaredsoftware.com/2017/12/idiomatic-redux-using-reselect-selectors/
Badal Saibo
  • 2,499
  • 11
  • 23
14

The answer to your questions is detailed in an FAQ here: https://github.com/reactjs/reselect#q-how-do-i-create-a-selector-that-takes-an-argument

In short, reselect doesn't support arbitrary arguments passed to selectors. The recommended approach is, instead of passing an argument, store that same data in your Redux state.

David L. Walsh
  • 24,097
  • 10
  • 61
  • 46
  • 7
    Sometimes this is not possible, though. In such cases, you can make your selector a factory that takes in the argument and returns the selector. Then you can "create" your selector in a `mapStateToProps` factory and use the instance of the selector that is scoped to your argument. The only downside I see with this approach is that you have to recreate the selector manually when the argument changes. – PhilippSpo Jan 23 '17 at 11:12
  • 2
    And imagine that you are storing the search query used against a list of todos: getTodosSelector and a getQuerySelector, since the query is considered as changed even if you search the same keyword twice, the cache is never hit. – Nik So Mar 03 '17 at 13:46
  • @PhilippSpo - wouldn't that break the ability to memoize the selectors? – Eliran Malka Dec 01 '19 at 11:56
  • 2
    @EliranMalka Not when you only re-create the selector when the relevant state/props actually change. Otherwise, yes it would break the memoization when re-creating the selector in every render. – PhilippSpo Dec 02 '19 at 12:06
  • 1
    This answer is way outdated, even the linked place says otherwise as the answer summarizes. It shouldn't be the accepted one. – sarimarton May 18 '22 at 16:37
8

what about returning a function from selector? getFilteredToDos is an example for that

// redux part
const state = {
  todos: [
    { state: 'done',     text: 'foo' },
    { state: 'time out', text: 'bar' },
  ],
};

// selector for todos
const getToDos = createSelector(
  getState,
  (state) => state.todos,
);

// selector for filtered todos
const getFilteredToDos = createSelector(
  getToDos,
  (todos) => (todoState) => todos.filter((toDo) => toDo.state === todoState);
);

// and in component
const mapStateToProps = (state, ownProps) => ({
  ...ownProps,
  doneToDos: getFilteredToDos()('done')
});
ColCh
  • 3,016
  • 3
  • 20
  • 20
  • 3
    This probably messes with the memoization, though. – Gert Sønderby Aug 22 '17 at 06:39
  • 2
    ...I actually don't think it does (unlike the answer below). There's no memoization per filter, but there is memoization for `getToDos`. This is as far as reselect can get while parametrized selectors are not supported. – Septagram Aug 12 '18 at 23:39
1

This is covered in the reselect docs under Accessing React Props in Selectors:

import { createSelector } from 'reselect'

const getVisibilityFilter = (state, props) =>
  state.todoLists[props.listId].visibilityFilter

const getTodos = (state, props) =>
  state.todoLists[props.listId].todos

const makeGetVisibleTodos = () => {
  return createSelector(
    [ getVisibilityFilter, getTodos ],
    (visibilityFilter, todos) => {
      switch (visibilityFilter) {
        case 'SHOW_COMPLETED':
          return todos.filter(todo => todo.completed)
        case 'SHOW_ACTIVE':
          return todos.filter(todo => !todo.completed)
        default:
          return todos
      }
    }
  )
}

export default makeGetVisibleTodos
const makeMapStateToProps = () => {
  const getVisibleTodos = makeGetVisibleTodos()
  const mapStateToProps = (state, props) => {
    return {
      todos: getVisibleTodos(state, props)
    }
  }
  return mapStateToProps
}

In this case, the props passed to the selectors are the props passed to a React component, but the props can come from anywhere:

const getVisibleTodos = makeGetVisibleTodos()

const todos = getVisibleTodos(state, {listId: 55})

Looking at the types below for Reselect:

export type ParametricSelector<S, P, R> = (state: S, props: P, ...args: any[]) => R;
export function createSelector<S, P, R1, T>(
  selectors: [ParametricSelector<S, P, R1>],
  combiner: (res: R1) => T,
): OutputParametricSelector<S, P, T, (res: R1) => T>;

We can see there isn't a constraint on the type of props (the P type in ParametricSelect), so it doesn't need to be an object.

mowwwalker
  • 16,634
  • 25
  • 104
  • 157
0

Here's a selector with parameters that is truly memoized (beyond the last call, unlike the createSelector from Reselect):

const pokemon = useSelector(selectPokemonById(id));
import memoize from 'lodash.memoize';
import { createSelector } from '@reduxjs/toolkit';

const selectPokemonById = memoize((id) => {
  return createSelector(
    (state) => state.pokemons,
    (pokemons) => {
      const pokemon = pokemons.find(id => p.id === id);
      return pokemon;
    }
  )
})

The problem with createSelector is that it only memoizes the last parameter called (https://redux.js.org/usage/deriving-data-selectors#createselector-behavior)

createSelector only memoizes the most recent set of parameters. That means that if you call a selector repeatedly with different inputs, it will still return a result, but it will have to keep re-running the output selector to produce the result:

To overcome this problem, the idea is to use lodash's memoize function to memoize the selector for a given parameter.

More info here: https://dev.to/tilakmaddy_68/how-to-memoize-correctly-using-redux-reselect-20m7

Fire Druid
  • 246
  • 2
  • 9
-1

Another option:

const parameterizedSelector = (state, someParam) => createSelector(
  [otherSelector],
  (otherSelectorResult) => someParam + otherSelectorResult
);

And then use like

const mapStateToProps = state => ({
  parameterizedSelectorResult: parameterizedSelector(state, 'hello')
});

I am not sure about memoization/performance in this case though, but it works.

Tomas P. R.
  • 159
  • 1
  • 7
  • 15
    The problem with this approach is that you are creating the selector in each call. Hence, the memoization will not work at all. – rafahoro Apr 18 '19 at 15:27