How to avoid re-rendering a functional component when a certain prop changes?

146 views Asked by At

I have a PlayArea component with a number of Card components as children, for a card game.

The position of the cards is managed by the PlayArea, which has a state value called cardsInPlay, which is an array of CardData objects including positional coordinates among other things. PlayArea passes cardsInPlay and setCardsInPlay (from useState) into each Card child component.

Cards are draggable, and while being dragged they call setCardsInPlay to update their own position.

The result, of course, is that cardsInPlay changes and therefore every card re-renders. This may grow costly if a hundred cards make it out onto the table.

How can I avoid this? Both PlayArea and Card are functional components.

Here's a simple code representation of that description:

const PlayArea = () => {
  const [cardsInPlay, setCardsInPlay] = useState([]);

  return (
    <>
      { cardsInPlay.map(card => (
        <Card
          key={card.id}
          card={card}
          cardsInPlay={cardsInPlay}
          setCardsInPlay={setCardsInPlay} />
      }
    </>
  );
}

const Card = React.memo({card, cardsInPlay, setCardsInPlay}) => {
    const onDrag = (moveEvent) => {
       setCardsInPlay(
         cardsInPlay.map(cardInPlay => {
           if (cardInPlay.id === card.id) {
              return {
               ...cardInPlay,
               x: moveEvent.clientX,
               y: moveEvent.clientY
              };
           }
           return cardInPlay;
        }));
      };

      return (<div onDrag={onDrag} />);
   });

          
2

There are 2 answers

8
Patrick Roberts On BEST ANSWER

The problem is you're passing the entire cardsInPlay array to each Card, so React.memo() will still re-render each card because the props have changed. Only pass the element that each card needs to know about and it will only re-render the card that has changed. You can access the previous cardsInPlay using the functional update signature of setCardsInPlay():

const PlayArea = () => {
  const [cardsInPlay, setCardsInPlay] = useState([]);
  const cards = cardsInPlay.map(
    card => (
      <Card
        key={card.id}
        card={card}
        setCardsInPlay={setCardsInPlay} />
    )
  );

  return (<>{cards}</>);
};

const Card = React.memo(({ card, setCardsInPlay }) => {
  const onDrag = (moveEvent) => {
    setCardsInPlay(
      cardsInPlay => cardsInPlay.map(cardInPlay => {
        if (cardInPlay.id === card.id) {
          return {
            ...cardInPlay,
            x: moveEvent.clientX,
            y: moveEvent.clientY
          };
        }
        return cardInPlay;
      })
    );
  };

  return (<div onDrag={onDrag} />);
});
1
Easwar On

It depends on how you pass cardsInPlay to each Card component. It doesn't matter if the array in state changes as long as you pass only the required information to child.

Eg:

<Card positionX={cardsInPlay[card.id].x} positionY={cardsInPlay[card.id].y} /> 

will not cause a re-render, because even i the parent array changes, the instance itself is not getting a new prop. But if you pass the whole data to each component :

<Card cardsInPlay={cardsInPlay} /> 

it will cause all to re-render because each Card would get a new prop for every render as no two arrays,objects are equal in Javascript.

P.S : Edited after seeing sample code