5

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.

10
  • What does state looks like? Commented Aug 22, 2020 at 13:49
  • @Yousaf updated with the initial state Commented Aug 22, 2020 at 13:50
  • 1
    Problem in the first code example is that it is modifying the state directly. { ...state } - This won't clone the objects inside the state. Meaning you are mutating the cards array directly. This doesn't changes the state.cards, hence useEffect is not triggered. Mapping over the state.cards is the way to do it. Commented Aug 22, 2020 at 13:53
  • 1
    { ...state } - This is making a new object, copying the properties of state in the new object but objects are reference types, so you are basically creating a property named cards in the new object but its value is the same array reference which was pointed to by cards property in state object. Commented Aug 22, 2020 at 14:01
  • 1
    @Kevin fair enough. I have a feeling it's catching the correct value on rerender. You could simply use passedCards.length to pass the correct value in the same useEffect cycle. Commented Aug 22, 2020 at 15:15

1 Answer 1

8

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.

Sign up to request clarification or add additional context in comments.

1 Comment

'keys' in the array? Your comment doesnt make a lot of sense or I am missing details to help you. If you want to force an update update the reference of you pass along as a prop to another component. The above example does it for arrays

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.