Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correctly use a curried selector function with react-redux's useSelector hook?

I am using react-redux with hooks, and I need a selector that takes a parameter that is not a prop. The documentation states

The selector function does not receive an ownProps argument. However, props can be used through closure (see the examples below) or by using a curried selector.

However, they don't provide an example. What is the proper way to curry as described in the docs?

This is what I've done and it seems to work, but is this right? Are there implications from returning a function from the useSelector function (it seems like it would never re-render?)

// selectors
export const getTodoById = state => id => {
  let t = state.todo.byId[id];
  // add display name to todo object
  return { ...t, display: getFancyDisplayName(t) };
};

const getFancyDisplayName = t => `${t.id}: ${t.title}`;

// example component
const TodoComponent = () => {
   // get id from react-router in URL
   const id = match.params.id && decodeURIComponent(match.params.id);

   const todo = useSelector(getTodoById)(id);

   return <span>todo.display</span>;
}
like image 429
user210757 Avatar asked Aug 12 '19 15:08

user210757


People also ask

Can I use selector in reducer?

It's not typically possible to use selectors inside of reducers, because a slice reducer only has access to its own slice of the Redux state, and most selectors expect to be given the entire Redux root state as an argument.

Can I use useSelector multiple times?

You may call useSelector() multiple times within a single function component. Each call to useSelector() creates an individual subscription to the Redux Store.

How do you call a selector in React?

You should call the selector on the connect function like this: import { connect } from 'react-redux'; import getVisibleTodos from 'your/selector/file'; function YourComponent({ visibleTodos }) { // You can access visibleTodos inside your component // because now it's on the props return ( <div> //...

What is the use of useSelector in React?

useSelector and useDispatch are a set of hooks to use as alternatives to the existing connect() higher-order component. The equivalent of map state to props is useSelector. It takes in a function argument that returns the part of the state that you want. The equivalent of map dispatch to props is useDispatch.


2 Answers

When the return value of a selector is a new function, the component will always re-render on each store change.

useSelector() uses strict === reference equality checks by default, not shallow equality

You can verify this with a super simple selector:

const curriedSelector = state => () => 0;

let renders = 0;
const Component = () => {
  // Returns a new function each time
  // triggers a new render each time
  const value = useSelector(curriedSelector)();
  return `Value ${value} (render: ${++renders})`;
}

Even if the value is always 0, the component will re-render on each store action since useSelector is unaware that we're calling the function to get the real value.

But if we make sure that useSelector receives the final value instead of the function, then the component only gets rendered on real value change.

const curriedSelector = state => () => 0;

let renders = 0;
const Component = () => {
  // Returns a computed value
  // triggers a new render only if the value changed
  const value = useSelector(state => curriedSelector(state)());
  return `Value ${value} (render: ${++renders})`;
}

Conclusion is that it works, but it's super inefficient to return a new function (or any new non-primitives) from a selector used with useSelector each time it is called.

props can be used through closure (see the examples below) or by using a curried selector.

The documentation meant either:

  • closure useSelector(state => state.todos[props.id])
  • curried useSelector(state => curriedSelector(state)(props.id))

connect is always available, and if you changed your selector a little, it could work with both.

export const getTodoById = (state, { id }) => /* */

const Component =  props => {
  const todo = useSelector(state => getTodoById(state, props));
}
// or
connect(getTodoById)(Component)

Note that since you're returning an Object from your selector, you might want to change the default equality check of useSelector to a shallow equality check.

import { shallowEqual } from 'react-redux'

export function useShallowEqualSelector(selector) {
  return useSelector(selector, shallowEqual)
}

or just

const todo = useSelector(state => getTodoById(state, id), shallowEqual);

If you're performing costly computations in the selector or the data is deeply nested and performance becomes a problem, take a look at Olivier's answer which uses memoization.

like image 138
Emile Bergeron Avatar answered Sep 18 '22 13:09

Emile Bergeron


Here is a solution, it uses memoïzation to not re-render the component on each store change :

First I create a function to make selectors, because the selector depends on the component property id, so I want to have a new selector per component instances.

The selector will prevent the component to re-render when the todo or the id prop hasn't changed.

Lastly I use useMemo because I don't want to have more than one selector per component instance.

You can see the last example of the documentation to have more information

// selectors
const makeGetTodoByIdSelector = () => createSelector(
   state => state.todo.byId,
   (_, id) => id,
   (todoById, id) => ({
       ...todoById[id], 
       display: getFancyDisplayName(todoById[id])
   })
);

const getFancyDisplayName = t => `${t.id}: ${t.title}`;

// example component
const TodoComponent = () => {
  // get id from react-router in URL
  const id = match.params.id && decodeURIComponent(match.params.id);

  const getTodoByIdSelector = useMemo(makeGetTodoByIdSelector, []);

  const todo = useSelector(state => getTodoByIdSelector(state, id));

  return <span>todo.display</span>;
}
like image 45
Olivier Boissé Avatar answered Sep 19 '22 13:09

Olivier Boissé