javascriptreactjsreduxmemoizationreselect

How to avoid re-render in Redux based on the outputs of .filter()?


I have a simple scenario in which I want to access my store.entities, but only those which are active. I understand the following is bad because a new reference is given each time:

const activeEntities = useSelector(
  state => state.entities.filter(entity => entity.active)
);

So my solution is just to move the filtering outside the selector like so:

const activeEntities = useSelector(state => state.entities)
  .filter(entity => entity.active);

I'm still new to Redux, but I believe this now means the component will only re-render on change to state.entities.

My question is: can this made any better at all by memoization?

I understand the following can be written:

const selectActiveEntities = createSelector(
  (state) => state.entities.items,
  (items) => items.filter(entity => entity.active)
);

But in my mind, this is functionally equivalent to what I wrote in my second example.

In an ideal world, I would like the component to only be re-rendered when there is a change in the active entities (i.e. the underlying values of the filter function change). How can I achieve this otherwise?


Solution

  • You are right about the point that even the third example is equivalent in terms of the rendering it will cause.

    How createSelector() works is that the outputSelector will not run if the inputSelector output does not change, but if your state array reference changes, it will run. And in that case, since the output selector is using a .filter(), a new array will be created even if the active values are still the same.

    Since you want to prevent rerender based on the active values you can use a customEquality function, which is passed in as the second argument of useSelector.

    For example if you have the ids of each of your entities, ensure that you only rerender when they change:

    
    
    const idCheck = (prevArr,newArr) => {
      //create array of ids
      const prevArrIds = prevArr.map(({id}) => id);
      const newArrIds = newArr.map(({id}) => id);
      
      //sort array
      prevArrIds.sort((a,b) => a-b);
      newArrIds.sort((a,b) => a-b);
    
      //join the array into a string and compare 
      return prevArrIds.join('') !== newArrIds.join('')
    
    };
    
    ...
    
    
    const activeEntities = useSelector(state => state.entities.filter(entity => entity.active), idCheck);
    

    Of course the above idCheck will now always run when the store value changes.

    The optional comparison function also enables using something like Lodash's _.isEqual() or Immutable.js's comparison capabilities. There are is shallowEqual from react-redux.

    A minor optimization you can do is use the selector created from createSelector and then still use this custom equality method.