ReactJs a state is triggeing in a loop when I use a selector normal function

115 Views Asked by At

Main problem: I tried to use a function inside the selector to reestructur the data and join another variable, in this case my group and put together with their children as items, the problem is that the function is called every time on an infinite loop despite the state is not being altered.

I have this selector: const groups = useSelector(state => selectProductGroups(state));

And the function is this one:

  const groups = state.PlatformsReducer.groups;
  const items = state.PlatformsReducer.items;
  return groups.reduce((ac, g) => {
    g.items = items.filter(i => i.groupId == g.productNumber);
    if (ac[g.platformId]) {
      ac[g.platformId].push(g);
    } else {
      ac[g.platformId] = [g];
    }
    return ac;
  }, {});
};

So when I use a useEffect to detect if the groups variable has changed the useEffect is triggered in a loop despite the variable groups still empty.

Do you know why? or How to prevent this.

I now the problem is the function in the selector, but I don't know how to prevent this case.
1

There are 1 best solutions below

0
On BEST ANSWER

This has to do with what the useSelector hook does internally.

useSelector runs your selector and checks if the result is the same as the previously received result (reference comparison). If the results differ then the new result is stored and a rerender is forced. If the results are the same then the old result is not replaced and no rerender is triggered.

What this does mean is that every time the store updates, even if it is an unrelated part of the state, your complex function will be run to determine whether the result has changed. In your case it is always a new reference and therefore always a change.

I think the best way to handle this is to keep your selectors as simple as possible, or use some form of more complex memoization like provided by reselect.

Below is an example of how you might be able to keep your selectors simple but still achieve an easy way to reuse your product group selection using a custom hook.

const useProductGroups = () => {
    // Get groups from the store. 
    // As the selector does not create a new object it should only 
    // trigger a rerender when groups changes in the store.
    const groups = useSelector(state => state.PlatformsReducer.groups);

    // Get items from the store, 
    // As the selector does not create a new object it should only 
    // trigger a rerender when items changes in the store.
    const items = useSelector(state => state.PlatformsReducer.items);

    // Reduce the group collection as desired inside of a useMemo 
    // so that the reduction only occurs when either items or groups 
    // changes.
    const productGroups = useMemo(() => {
        return groups.reduce((ac, g) => {
            g.items = items.filter(i => i.groupId == g.productNumber);

            if (ac[g.platformId]) {
                ac[g.platformId].push(g);
            } else {
                ac[g.platformId] = [g];
            }

            return ac;
        }, {});
    }, [groups, items] /* dependency array on items / groups */);

    // return the calculated product groups
    return productGroups;
}

You can then use the custom hook in your function components:

const groups = useProductGroups();