Arch1tect
Arch1tect

Reputation: 4281

React shallow copy still triggers re-render?

From what I learnt about React, you should not mutate any objects otherwise React doesn't know to re-render, for example, the following example should not trigger re-render in the UI when button is clicked on:

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App({ input }) {
  const [items, setItems] = useState(input);

  return (
    <div>
      {items.map((item) => (
        <MyItem item={item}/>
      ))}
      <button
        onClick={() => {
          setItems((prevItems) => {
            return prevItems.map((item) => {
              if (item.id === 2) {
                item.name = Math.random();
              }
              return item;
            });
          });
        }}
      >
        Update wouldn't work due to shallow copy
      </button>
    </div>
  );
}

function MyItem ({item}) {
  const name = item.name
  return <p>{name}</p>
}

ReactDOM.render(
  <App
    input={[
      { name: "apple", id: 1 },
      { name: "banana", id: 2 }
    ]}
  />,
  document.getElementById("container")
);

You can try above code here

And the correct way to update the array of objects should be like below (other ways of deep copying would work too)

 setItems((prevItems) => {
    return prevItems.map((item) => {
               if (item.id === 2) {
                   # This way we return a deepcopy of item
                   return {...item, name: Math.random()}
               }
               return item;
            });
});

Why does the 1st version works fine and the UI is updated right away even though I'm just updating the original item object?

Upvotes: 2

Views: 1404

Answers (2)

dhilt
dhilt

Reputation: 20754

Render happens due to .map that creates new array. If you do something like prev[1].name = "x"; return prev; in your hook, this will not perform an update. Per reactjs doc on setState with function argument:

If your update function returns the exact same value as the current state, the subsequent rerender will be skipped completely.

Update.

Yes, speaking of parent-child interaction, item would be the same (by reference), but the child props would differ. You have MyItem({ item }) and this item is being destructured from props, say MyItem(props), and this props changes because of the parent source is changed.

So each time you map the list, you explicitly ask the parent to render its children, and the fact that some (or all) of the child's param is not changed does not matter. To prove this you may remove any params from the child component:

  {items.map(() => ( <MyItem /> ))}
function MyItem () {
  return <p>hello</p>
}

MyItem will be invoked each time you perform items update via state hook. And its props will always be different from the previous version.

Upvotes: 2

Eric Cheng
Eric Cheng

Reputation: 517

if your setItem setting a new object, your page with a state change will re-render in your logic, you performed a shallow copy, which:

  • a shallow copy created a new object, and copy everything of the 1st level from old object.

Shallow copy and deep copy also created a new object, they both trigger a re-render in React.

The different of shallow copy and deep copy is: from 2nd level of the old object, shallow copy remains the same objects, which deep copy will create new objects in all level.

Upvotes: 0

Related Questions