Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React - Filtering objects in store causes recursion/infinity loop

I use redux store in my react application to store data such as my expenses.

Now I use these expenses in one of the components to calculate a TODO badge count to show like so

const [badgeValue, setBadgeValue] = useState(0);
const expenses = useSelector(({ expenses }: RootState) => expenses.items);

useEffect(() => {
    const setBadge = async () => {
        const badgeNumber = await calculateRequestBadgeNumber(expenses);
        setBadgeValue(badgeNumber);
    };

    setBadge();

    return () => setBadgeValue(0);
}, [expenses]);

This works as expected. Now however, I'm experiencing a weird side effect when I want to filter out deleted expenses like so:

 const expenses = useSelector(({ expenses }: RootState) => 
     expenses.items.filter((expense: Expense) => !expense.deleted)
 );

This results in an endless loop of useEffects being called. Why is this happening?

like image 872
phoebus Avatar asked Mar 21 '26 18:03

phoebus


1 Answers

As with most things involving React hooks, the identity of the objects involved (e.g. what you test for with ===) is important here. When the identity of expenses differs from the last render, your effect is triggered.


Selector A

This hook returns the items property of the expenses property of your Redux state. If the state doesn't change, neither will the identity of the items property.

const expenses = useSelector(({ expenses }: RootState) => expenses.items);

Selector B

This returns the result of calling filter on the items array. The filter method returns a brand new array.

const expenses = useSelector(({ expenses }: RootState) => 
     expenses.items.filter((expense: Expense) => !expense.deleted)
);

What you're observing is Selector B returning a new value each time the component renders, thus the effect is triggered too often.

Now - useSelector can cache its return value to reuse if the state hasn't changed but only if the identity of the selector function you pass to it remains the same. You can ensure this by naming the selector in the module scope as opposed to the component.

const filteredExpenses = ({ expenses }: RootState) =>
  expenses.items.filter((expense: Expense) => !expense.deleted);

const MyComponent = () => {
  const [badgeValue, setBadgeValue] = useState(0);
  const expenses = useSelector(filteredExpenses);
  useEffect(() => {
    const setBadge = async () => {
      const badgeNumber = await calculateRequestBadgeNumber(expenses);
      setBadgeValue(badgeNumber);
    };

    setBadge();

    return () => setBadgeValue(0);
  }, [expenses]);
  return <StuffToRender />;
};
like image 166
backtick Avatar answered Mar 23 '26 09:03

backtick