1

I have an array of card component. I will often be adding a card on a user action(like click a button) or the user is able to remove a card. However, since it's useState when the state changes it gets re rendered. But In the case I have 3 cards in my array and that I add a 4th one I really don't want to re render the other 3 when no change happened to them but it's just that they are being in an array made from useState.

The requirement is that it doesn't re render existing component whether I add or remove an element from the array.

I've tried useState & useRef and custom hook and no luck there. With useRef it didn't re render when I needed it to re render. Like it didn't re render the existing but also didn't re render to show the new one. The custom hook combine the add and remove feature but still used useState from within.

Here is a smaller version of the issue in a sandbox. For the sake of a quick example, I'm hardcoding the remove function. In the console, you'll see the console log printing when you add or remove and that's inside the card component(shouldn't happen ideally) https://codesandbox.io/s/no-rerender-array-element-jvu6q5

Thanks for any help!

import "./styles.css";
import React, { useEffect, useRef, useState, useContext } from "react";

const fakeData1 = {
  Card1: [1, 2, 3, 4]
};
const fakeData2 = {
  Card2: [5, 6, 7, 8]
};

const fakeObject = { fakeData1 };
export default function App() {
  const [cardArray, setCardArray] = useState(fakeObject);

  const addCard = () => {
    setCardArray((entityState) => ({
      ...entityState,
      fakeData2
    }));
  };

  const Card = ({ id, index, item }) => {
    console.log("Rendering Card: ", item);
    const handleRemove = (event: any | MouseEvent) => {
      if (event.type == "click" || event.type == "keydown") {
        setCardArray((entityState) => {
          const updatedData: any = { ...entityState };
          delete updatedData["fakeData2"];
          return updatedData;
        });
      }
    };
    return (
      <div style={{ border: "black solid 2px", padding: "50px 0" }}>
        <h1>Card - {id}</h1>
        <div>Content: {Object.values(item)}</div>
        <button onClick={handleRemove}>Remove</button>
      </div>
    );
  };

  return (
    <div className="App">
      <button onClick={addCard}>Add a Card</button>
      {Object.values(cardArray)
        .flat()
        .map((item: any, index) => {
          return <Card id={index} key={index} item={item} />;
        })}
    </div>
  );
}


Ikura
  • 188
  • 1
  • 11
  • Does this answer your question? [How can I prevent re-render after state changed in React Hooks?](https://stackoverflow.com/questions/69965614/how-can-i-prevent-re-render-after-state-changed-in-react-hooks) – Quimbo May 15 '23 at 01:42
  • It's not recommended to define components inside another component. It will be recreated every time the parent component re-renders, which can lead to unexpected behaviors. – ivanatias May 15 '23 at 05:25

2 Answers2

0

you can use React.memo to prevent components from re-rendering without any updates.like this:

import "./styles.css";
import React, { useEffect, useRef, useState, useContext, memo } from "react";

const fakeData1 = {
  Card1: [1, 2, 3, 4]
};
const fakeData2 = {
  Card2: [5, 6, 7, 8]
};

const Card = memo(({ id, index, item, setCardArray }) => {
  console.log("Rendering Card: ", item);
  const handleRemove = (event: any | MouseEvent) => {
    if (event.type == "click" || event.type == "keydown") {
      setCardArray((entityState) => {
        const updatedData: any = { ...entityState };
        delete updatedData["fakeData2"];
        return updatedData;
      });
    }
  };
  return (
    <div style={{ border: "black solid 2px", padding: "50px 0" }}>
      <h1>Card - {id}</h1>
      <div>Content: {Object.values(item)}</div>
      <button onClick={handleRemove}>Remove</button>
    </div>
  );
});


const fakeObject = { fakeData1 };
export default function App() {
  const [cardArray, setCardArray] = useState(fakeObject);

  const addCard = () => {
    setCardArray((entityState) => ({
      ...entityState,
      fakeData2
    }));
  };

  
  return (
    <div className="App">
      <button onClick={addCard}>Add a Card</button>
      {Object.values(cardArray)
        .flat()
        .map((item: any, index) => {
          return <Card setCardArray={setCardArray} id={index} key={index} item={item} />;
        })}
    </div>
  );
}

And i suggest that the key of the component should be set to a unique value, so that the component can be executed in the way we expected. Hope it helps you.

Sky Clong
  • 141
  • 1
  • 8
  • Thanks! Your answer works, the other answer used useCallback on the remove function. Memo is better for the return and useCallback is better for the work/calculation of the function, but in this scenario does it matter whichever? – Ikura May 16 '23 at 02:00
  • @lkura If you don't want to trigger component re-render, you must ensure that the props passed to the component have not changed. `React memo` will shallowly compare props. Objects or methods compare reference addresses, so if you pass `onRemove`, use `useCallback` cache. In case a new `onRemove` is redefined after component re-render, causing sub-components to re-render. In my answer, there is no redefinition method, because setCardArray will not change with component updates, both ways are fine. – Sky Clong May 16 '23 at 02:42
  • An issue I ran into with memo(maybe that's not the issue), is that my object with all the cards, (let's say I have 2 cards) when I removed one at the beginning of my data structure, it replaced the first card with the content of the second. But if I remove the second card it didn't. So like the position matters? – Ikura May 16 '23 at 22:03
  • Because of the react key, the two renderings are handled differently for react. Once the corresponding key as index = 1 is deleted, the component will unmount, and once the corresponding key is retained as index = 0, the component will re-render. you can read about https://stackoverflow.com/a/46735689/11104708 – Sky Clong May 17 '23 at 01:54
0

What you need is to memo-ize your state and functions, you can use React.memo memoize (make it so that it doesn't refresh unecesarly) and useCallback to memo-ize the removeCard function, you also need to move the removeCard funct to outside of the card component and pass it as a prop as every time a new card is made it creates a new removeCard function causing a re-render you can update your code like so e(Im based it on the codeSandbox):

import "./styles.css";
import React, {
  useEffect,
  useRef,
  useState,
  useContext,
  useCallback
} from "react";

const fakeData1 = {
  Card1: [1, 2, 3, 4]
};
const fakeData2 = {
  Card2: [5, 6, 7, 8]
};
const Card = React.memo(({ id, item, onRemove }) => {
  console.log("Rendering Card: ", item);

  return (
    <div style={{ border: "black solid 2px", padding: "50px 0" }}>
      <h1>Card - {id}</h1>
      <div>Content: {Object.values(item)}</div>
      <button onClick={onRemove}>Remove</button>
    </div>
  );
});

const fakeObject = { fakeData1 };
export default function App() {
  const [cardArray, setCardArray] = useState(fakeObject);

  const addCard = () => {
    setCardArray((entityState) => ({
      ...entityState,
      fakeData2
    }));
  };

  const removeCard = useCallback(() => {
    setCardArray((entityState) => {
      const updatedData = { ...entityState };
      delete updatedData["fakeData2"];
      return updatedData;
    });
  }, []);

  return (
    <div className="App">
      <button onClick={addCard}>Add a Card</button>
      {Object.values(cardArray)
        .flat()
        .map((item, index) => {
          const cardName = `Card${index + 1}`;
          return (
            <Card id={index} key={index} item={item} onRemove={removeCard} />
          );
        })}
    </div>
  );
}
Decod621
  • 114
  • 1
  • 4