useSelector destructuring vs multiple calls
Asked Answered
W

5

60

Recently I was been reading react-redux docs https://react-redux.js.org/next/api/hooks And there was a section related to Equality Comparisons and Updates, which says:

Call useSelector() multiple times, with each call returning a single field value.

First approach:

const { open, importId, importProgress } = useSelector((importApp) => importApp.productsImport);

Second approach:

const open = useSelector((importApp) => importApp.productsImport.open);
const importId = useSelector((importApp) => importApp.productsImport.importId );
const importProgress = useSelector((importApp) => importApp.productsImport.importProgress);

So is there any real differences? Or due to destructuring useSelector hook will have troubles with checking refences?

Wirework answered 27/11, 2019 at 14:18 Comment(0)
A
72

Just to lay the groundwork: upon an action being dispatched, the selector you pass to useSelector() will be called. If the value it returns is different to the value returned last time an action was dispatched, the component will re-render.

Destructing is indeed the wrong approach, but the top answer here is completely irrelevant. The docs refer to a scenario where the selector is creating a new object every time, like you might do in a mapStateToProps() function. That would cause the component to re-render every single time an action is dispatched, regardless of what that action does, because the value returned by the selector is technically a different object in memory even if the actual data hasn't changed. In that case, you need to worry about strict equality and shallow equality comparisons. However, your selector is not creating a new object every time. If a dispatched action doesn't modify importApp.productsImport, it will be the exact same object in memory as before, rendering all of this moot.

Instead, the issue here is that you are selecting an entire slice of state, when you only actually care about a few particular properties of that slice. Consider that importApp.productsImport probably has other properties besides just open, importId, and importProgress. If those other properties change, then your component will needlessly re-render even though it makes no reference to them. The reason for this is simple: the selector returns importApp.productsImport, and that object changed. Redux has no way of knowing that open, importId, and importProgress were the only properties you actually cared about, because you didn't select those properties; you selected the whole object.

Solutions

So, to select multiple properties without needless re-renders, you have two options:

  • Use multiple useSelector() hooks, each selecting a single property in your store.
  • Have a single useSelector() hook and a single selector that combines multiple properties from your store into a single object. You could do this by:
    • Using a memoized selector from reselect.
    • Simply writing a function that creates a new object from specific properties of state and returns it. If you did this, you would then have to worry about strict equality and shallow equality comparisons.

For this purpose, I feel like multiple useSelector() hooks is actually the way to go. The docs make a point of mentioning that

Each call to useSelector() creates an individual subscription to the Redux store.

but whether multiple calls would actually incur a real performance penalty compared to a single call purely for this reason, I think, remains to be seen, and it seems to me that worrying about this is probably over-optimisation unless you have a huge app with hundreds or thousands of subscriptions. If you use a single useSelector() hook, then at that point you're basically just writing a mapStateToProps function, which I feel like defeats a lot of the allure of using the hook to begin with, and especially so if you're writing TypeScript. And if you then want to destructure the result, that makes it even more cumbersome. I also think using multiple hooks is definitely more in the general spirit of the Hooks API.

Alvar answered 6/8, 2020 at 6:22 Comment(1)
Very useful answer. You basically laid the groundwork to understand why createSelector is necessary. This should be linked to in the redux docs!Cobber
C
24

When an action is dispatched to the Redux store, useSelector() only does a strict "===" reference comparison. This does not do a shallow check.

So, if you want to retrieve multiple values from the store, you must call useSelector() multiple times, with each call returning a single field value from state. Or use shallowEqual in react-redux:

import { shallowEqual, useSelector } from 'react-redux'

const data = useSelector(state => state.something, shallowEqual)

Refer to https://react-redux.js.org/next/api/hooks#equality-comparisons-and-updates for detailed explanation.

Climactic answered 13/2, 2020 at 6:43 Comment(2)
This is correct. Here's a sandbox to illustrate the point: codesandbox.io/s/…Anthurium
@Anthurium Thanks for creating the example. It is very useful.Ventriloquize
M
7

How to select multiple states ::

const [village, timeZone, date] = useSelector((state) => [
    state.active_village,
    state.time_zone,
    state.date,
  ], shallowEqual);
Miraculous answered 17/2, 2021 at 14:2 Comment(3)
It will still re-render the componentLewls
This is very bad idea, every time you're returning a new object, so component will re-render in unexpected cases.Shortlived
no it doesn't re-render, because he used shallowEqual, which would return true for two array of [string, timezone, date] types, try it out. proposals.es/proposals/Object.shallowEqualAzotobacter
P
5

This answer doesn't have many technical details. Please go through other answers for better understanding. Here, I'm just mentioning use case scenarios only. Hoping you will find it helpful:

Assumption: productsImport has only the fields you have mentioned (open, importId, importProgress)

1. Let say you are going to get all the fields in a single component using useSelector then I would say that you should go with First Approach. As using Second Approach will going to add few extra lines of code with no other benefits. Anyhow your component is going to re-render whenever any of the field's values get updated.

2. Now, Let's say you are using and updating importId in one component(Component1) and the remaining fields in another component(Component2). And, also these components should be rendered on the same window/screen. Then you should use Second Approach.

Component1:
    const importId = useSelector((importApp) => importApp.productsImport.importId );

Component2:
 const open = useSelector((importApp) => importApp.productsImport.open);
 const importProgress = useSelector((importApp) => importApp.productsImport.importProgress);
 
if you want to use only one selector:
const [open, importProgress] = useSelector((importApp) =>[importApp.productsImport.open, importApp.productsImport.importProgress], 
shallowEqual);
'Note: Here do not forget to use shallowEqual. Otherwise, your component will rerender on every useSelector() call.'

If you will use First Approach here, then your both component will reRender everytime you update any of the fields.

Provision answered 29/5, 2021 at 19:38 Comment(0)
D
0

The first option is better: const { open, importId, importProgress } = useSelector((importApp) => importApp.productsImport);

Because:

  1. It uses the useSelector hook once so there won't be unnecessary rerenders.
  2. Better readability and less code
Doubletongue answered 22/10, 2023 at 10:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.