Use reselect selector with parameters
Asked Answered
A

8

69

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

Ailurophile answered 27/10, 2016 at 17:44 Comment(0)
A
74

Updated: 4 December 2023

No changes compared to 4.1, but the updated document of version 5.0.0 using Typescript. Read document for more detail about the exceptional case.

// selector.ts
const selectAvailableItems = createSelector(
  [
    // First input selector extracts items from the state
    (state: RootState) => state.items,
    // Second input selector forwards the category argument
    (state: RootState, category: string) => category,
    // Third input selector forwards the ID argument
    (state: RootState, category: string, id: number) => id
  ],
  // Output selector uses the extracted items, category, and ID
  (items, category, id) =>
    items.filter(item => item.category === category && item.id !== id)
)

interface RootState {
  items: {
    id: number
    category: string
    vendor: { id: number; name: string }
  }[]
  // ... other state properties ...
}
// App.tsx
const items = selectAvailableItems(state, 'javascript', 10);
// Another way if you're using redux hook:
const items = useSelector(state => selectAvailableItems(state, 'javascript', 10));

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);
Alvinaalvine answered 15/4, 2020 at 3:35 Comment(6)
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)Mccaleb
It is explained here. github.com/reduxjs/…Guest
your proposed solution it's working but ts it's throwing me this error Expected 1 arguments, but got 2 any ideas?Aright
I've found the typing solution at github.com/reduxjs/reselect/issues/459#issuecomment-758649998Aright
Nice solution, What should I do if I want to pass three or four params?Macrogamete
@Macrogamete Did you try returning the object in the second selector? 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, category1, category2) =>{ category1, category2 } ], // Output selector gets (items, category) as args (items, { category1, category2 }) => items.filter(item => item.category === category1 or item.category === category2) );Alvinaalvine
B
18

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/
Breakaway answered 18/11, 2020 at 5:52 Comment(2)
Nice solution, What should I do if I want to pass three or four params?Macrogamete
@Macrogamete "Since input selectors are given all arguments, they can extract the additional arguments and pass them to the output selector"1 you can pass the third and fourth parameter similar to how we passed the second parameter in the example above, and extract it the same way. For a new parameter name, you would need to add (_, _, name) => name below the (_, isDone) => isDone which would be available to the output selector as (todo, isDone, name) => ...Breakaway
D
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.

Devisee answered 27/10, 2016 at 21:57 Comment(5)
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.Foreigner
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.Empiricism
@Foreigner - wouldn't that break the ability to memoize the selectors?Rupe
@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.Foreigner
This answer is way outdated, even the linked place says otherwise as the answer summarizes. It shouldn't be the accepted one.Sheryl
G
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')
});
Gathard answered 3/2, 2017 at 18:51 Comment(2)
This probably messes with the memoization, though.Cutch
...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.Warden
W
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.

Wohlen answered 23/12, 2020 at 1:38 Comment(0)
R
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

Recrudesce answered 16/6, 2023 at 15:9 Comment(0)
R
0

Create a selector factory and memoize returning selector with useMemo. Here's an example:

const createMySelector = (p1, p2) => createSelector(
  foobarSelector,
  (foobar) => foobar[p1][p2]
);

function MyComponent({p1, p2}) {
  const mySelector = useMemo(() => createMySelector(p1, p2), [p1, p2]);
  const myValue = useSelector(mySelector);
}

You can combine useMemo and useSelector into useSelectorFactory:

function MyComponent({p1, p2}) {
  const myValue = useSelectorFactory(createMySelector, p1, p2);
}
Rarebit answered 4/12, 2023 at 20:58 Comment(0)
W
-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.

Walhalla answered 26/7, 2017 at 14:28 Comment(1)
The problem with this approach is that you are creating the selector in each call. Hence, the memoization will not work at all.Delano

© 2022 - 2025 — McMap. All rights reserved.