useSelector with equality check still triggers useEffect
See original GitHub issueDo you want to request a feature or report a bug? I have a feeling this is by design, and if so we should look to update the documentation for https://react-redux.js.org/next/api/hooks#equality-comparisons-and-updates
as it reads like creating a memoized selector or using an equalityFn argument in useSelector have the same result in the component rendering process.
What is the current behavior?
I have a simplified example of the example. https://codesandbox.io/s/great-tree-7r9if.
setToggle is used as a forceUpdate of the component independent of the two changing values in the store, testString
and testObject
.
The component is subscribed to testString and testObject as a memoized selector and a useSelector with equality check. Below are the examples in question.
on Button press with no force update (removing setToggle).
- testString useEffect not fired (as expected)
- testSelector useEffect not fired (as expected memoized selector with shallowEqual check)
- testObject useEffect not fired (as expected equalityFn check returns true, no new testObject value is returned).
on Button press with forced component update.
- testString useEffect not fired (as expected)
- testSelector useEffect not fired (as expected memoized selector with shallowEqual check)
- testObject useEffect is fired (as expected?? shallowEqual equalityFn means that this is not the cause of the re-render, but seems to return a different object reference and therefore will trigger the useEffect)
What is the expected behavior? Based on the documentation it seems that creating a memoized selector and using the equalityFn check will result in the same behaviour, so if the equalityFn is truthy then it returns the same store reference (not trigger re-render or useEffect call). I have a feeling this is not really possible with this api, so may we need to update the docs to clarify this and maybe recommend the memoized selector approach or many useSelector instances which return primitive values.
Issue Analytics
- State:
- Created 4 years ago
- Comments:6 (2 by maintainers)
Top GitHub Comments
I stumbled across this when i was trying to understand why my selector wasn’t working, the problem I didn’t notice is that useSelector has as strict comparison, so it’s good for strings but not really useful with the way we program in yee 'ol immutable state. The second argument for useSelector takes a comparator function, and I usually just stick in isEqual from lodash.
Hope this helps, break it down a little easier.
I’m not quite clear exactly what point you’re trying to make here. Can you clarify?
Also, I note that the
createSelector
call in the sandbox is actually useless. Any time you have avalue => value
output selector increateSelector
, you’re using it wrong.A better example of a memoized selector would be filtering a list of visible todos from the Redux todos demo. (I just showed how to convert that filtering to Reselect in a tutorial for Redux Starter Kit.)
That selector looks like:
Let’s say that the filter has been set to
SHOW_ALL
. If this selector weren’t memoized at all, it would be returning a new array every time any action is dispatched, because it’s always doing the filter work. However, with memoization, thetodos.filter()
call only happens if either thetodos
array or thefilter
value have changed from the last time.That means that if an action like
"INCREMENT"
is dispatched, this memoized selector will return the same array reference as last time.useSelector()
's default===
comparison will see that the result array is the same reference, and not force a re-render.For point of comparison, here’s what would happen if we did not memoize the selector, but did use
shallowEqual
as anequalityFn
. The non-memoized selector would return a new filtered array reference, butshallowEqual
would see that the old and new filtered arrays are equivalent (samelength
field, identical content references). So, the comparison would returntrue
, and againuseSelector()
would not force a re-render.However, all that only applies when a Redux action is dispatched. If the component starts to render for any other reason (parent re-rendering, a
useState()
setter being called, etc), the selector just returns whatever value it has. If we again used the non-memoized filtered todos selector, it would return a new array reference, and that would trigger auseEffect()
that has the returned array in its dependency list.