kevin
kevin

Reputation: 3508

React hooks: useEffect not being triggered on array of objects

For readability im going to strip out a lot of functionality in my examples. However, essentially I have a useEffect (shown below) that has a dependency that tracks the state.cards array of card objects. My assumption was that if that state.cards property changes then the useEffect should trigger. However, that's not exactly proving to be the case.

Below are two solutions that are being used. I want to use the first one since it's in constant time. The second, while fine, is linear. What confuses me is why the second option triggers the dependency while the first does not. Both are return a clone of correctly modified state.

This does not trigger the useEffect dependency state.cards.

      const newArr = { ...state };
      const matchingCard = newArr.cards[action.payload];   <-- payload = number
      matchingCard.correct += 1;
      matchingCard.lastPass = true;
      return newArr;

This does trigger the useEffect dependency state.cards.

      const newArr = { ...state };
      const cards = newArr.cards.map((card) => {
        if (card.id === action.payload.id) {
          card.correct += 1;
          card.lastPass = true;
        }
        return card;
      });
      return { ...newArr, cards };

useEffect

  useEffect(() => {
    const passedCards = state.cards.filter((card) => {
      return card.lastPass;
    });
    setLearnedCards(passedCards);
    const calculatePercent = () => {
      return (learnedCards.length / state.cards.length) * 100;
    };
    dispatch({ type: 'SET_PERCENT_COMPLETE', payload: calculatePercent() });
  }, [learnedCards.length, state.cards]);

State

const initialState = {
  cards: [],  <-- each card will be an object
  percentComplete: 0,
  lessonComplete: false,
};



Solution: Working solution using the first example:

      const newCardsArray = [...state.cards];
      const matchingCard = newCardsArray[action.payload];
      matchingCard.correct += 1;
      matchingCard.lastPass = true;
      return { ...state, cards: newCardsArray };

Why: Spreading the array state.cards creates a new shallow copy of that array. Then I can make modifications on that cloned array and return it as the new value assigned to state.cards. The spread array has a new reference and that is detected by useEffect.

Upvotes: 5

Views: 6735

Answers (1)

Chrissi Grilus
Chrissi Grilus

Reputation: 1375

My best guess is that in the second working example .map returns a new array with a new reference. In the first example you are just mutating the contents of the array but not the reference to that array.

I am not exactly sure how useEffect compares, but if I remember correctly for an object it is just all about the reference to that object. Which sometimes makes it difficult to use useEffect on objects. It might be the same with arrays too.

Why dont you try out:

const newCardsArray = [...state.cards]
// do your mutations here

should copy the array with a new ref like you did with the object.

Upvotes: 8

Related Questions