reactjsmemoizationmemo

React how to memoize a component that receives a very nested object/array as prop


React.memo uses a shallow comparison to determine if the props are equal, but I need to pass an object or array as prop, so I went into an areEqual condition, but the currentProps and nextProps values are always the same. I mean, the component does not render.

Lets say this:

export default function App() {
  const [data, setData] = useState([
    {
      name: "First name",
      amount: 0
    },
    {
      name: "Other name",
      amount: 0
    }
  ]);
  const [value, setValue] = useState("");

  return (
    <>
      <input
        type="text"
        placeholder="Type in for test"
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />
      <br />
      <input
        type="button"
        value="Click to increment first!"
        onClick={() => {
          const temp = [...data];
          temp[0].amount += 1;
          setData(temp);
        }}
      />
      <input
        type="button"
        value="Click to increment other!"
        onClick={() => {
          const temp = [...data];
          temp[1].amount += 1;
          setData(temp);
        }}
      />
      <br />

      <Child data={data} />
    </>
  );
}

and

const Child = ({ data }) => {
  const count = useRef(0);
  return (
    <>
      {data &&
        data.map((obj, index) => {
          return obj.name + "-" + obj.amount;
        })}
      <br />
      Count: {count.current++}
    </>
  );
};

const areEqual = (currentProps, nextProps) => {
  console.log(currentProps.data[0].amount, nextProps.data[0].amount);
  console.log(currentProps.data[1].amount, nextProps.data[1].amount);
  if (
    currentProps.data[0].amount === nextProps.data[0].amount &&
    currentProps.data[1].amount === nextProps.data[1].amount
  ) {
    return true;
  }
  return false;
};

export default memo(Child, areEqual);

but no matter what always currentProps and nextProps are returning the very same value: enter image description here

Everything is on this sandbox. What am I missing here?


Solution

  • The problem is with the object mutation. Create a new object reference instead.

    Don't

    const temp = [...data];
    temp[0].amount += 1;
    setData(temp);
    

    Do

    setData(
      data.map((item, index) =>
        index === 0 ? { ...item, amount: item.amount + 1 } : item
      )
    );
    

    Also works (using a temporary variable)

    If you prefer the mutation style of using a temp variable, you should avoid using the same object reference:

    const temp = [...data];
    temp[0] = { ...temp[0], amount: temp[0].amount + 1 };
    setData(temp);
    

    Why?

    but no matter what always currentProps and nextProps are returning the very same value

    currentProps and nextProps are different (the references of the data prop are different). You can check it by adding console.log(currentProps.data === nextProps.data) to your areEquals function (it will return false).

    By reusing the same object references, when you make the mutation on one object (for instance the one at index 0) it gets updated in both currentProps.data[0] and nextProps.data[0]. You can check it by adding console.log(currentProps.data[0] === nextProps.data[0]) to your areEquals function (it will return true).