React-redux: useSelector with equality check still triggers useEffect

Created on 16 Aug 2019  路  3Comments  路  Source: reduxjs/react-redux

Do 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.

Most helpful comment

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.

useSelector(state => state.someCoolMusicList, isEqual);

Hope this helps, break it down a little easier.

All 3 comments

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 a value => value output selector in createSelector, 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:

const selectVisibleTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case VisibilityFilters.SHOW_ALL:
        return todos
      case VisibilityFilters.SHOW_COMPLETED:
        return todos.filter(t => t.completed)
      case VisibilityFilters.SHOW_ACTIVE:
        return todos.filter(t => !t.completed)
      default:
        throw new Error('Unknown filter: ' + filter)
    }
  }
)

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, the todos.filter() call only happens if either the todos array or the filter 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 an equalityFn. The non-memoized selector would return a new filtered array reference, but shallowEqual would see that the old and new filtered arrays are equivalent (same length field, identical content references). So, the comparison would return true, and again useSelector() 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 a useEffect() that has the returned array in its dependency list.

Ok thanks for your quick response, I will close this as it is not a bug as explained above.

I had tried to simplify the createSelector for the example, I guess too far, is there an issue with using it this way to 'memoize' the object other than it being incorrect usage of reselect as it seems to be bailing out of the useEffect as desired, maybe I am missing something.

Maybe this instance is more suitable to use useSelector(()=> JSON.stringify(someObject)) or a custom useEffect hook that has shallow comparison to bail out, as I am not concerned about the actual re-rendering, but more about the useEffect which can be used to perform sideEffects.

Thanks again for the quick response.

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.

useSelector(state => state.someCoolMusicList, isEqual);

Hope this helps, break it down a little easier.

Was this page helpful?
0 / 5 - 0 ratings