What are risks or side effects of doing shallow copy when updating state in functional component?

Viewed 596

I've got the following small example and I assume that if I do a shallow copy before replacing state, that my sessions attribute will not be new, but just a reference to old. I've got the shallow copy defined as the variable newSpeakersDataShallow and the deep copy variable named newSpeakersDataDeep.

If you change what is passed into setSpeakerData, you'll see they both work the same. My question is, what are my risks? if I know that I will not be changing the sessions array, is it OK to use the shallow copy?

(This is just a simplified example so the answer isn't, might as well use deep since it's so easy to do in this case).

https://codesandbox.io/s/clever-varahamihira-ry9x4?file=/pages/index.js:0-1990

Same code as codesandbox:

import { useEffect, useState } from "react";

const data = [
  {
    id: "1",
    first: "Joe",
    last: "Smith",
    favorite: true,
    sessions: [
      {
        id: "32",
        title: "Rails powered by",
      },
      {
        id: "58",
        title: "Hello World to .NET 3.5 interoperable Web service",
      },
    ],
  },
  {
    id: "2",
    first: "Jon",
    last: "Jones",
    favorite: false,
    sessions: [
      {
        id: "1011",
        title: "scalability and deployability",
      },
    ],
  },
  {
    id: "3",
    first: "Sam",
    last: "Hulk",
    favorite: true,
    sessions: [],
  },
];

function Speakers() {
  const [speakersData, setSpeakersData] = useState([]);

  useEffect(() => {
    setSpeakersData(data);
  }, []);

  return (
      <>
        {speakersData.map(function (speaker) {
          return (
              <div key={speaker.id}>
                <button
                    onClick={() => {
                      const newSpeakersDataShallow = speakersData.map(function (rec) {
                        return speaker.id == rec.id
                            ? { ...rec, favorite: !rec.favorite }
                            : { ...rec };
                      });

                      const newSpeakersDataDeep = speakersData.map(function (rec) {
                        return speaker.id == rec.id
                            ? {
                              ...rec,
                              favorite: !rec.favorite,
                              sessions: [...rec.sessions],
                            }
                            : { ...rec };
                      });

                      // RESULTS ARE SAME WHETHER DEEP OR SHALLOW COPY USED
                      setSpeakersData(newSpeakersDataDeep);
                    }}
                >
                  Toggle Favorite
                </button>
                &nbsp;{speaker.id} {speaker.first} {speaker.last}{" "}
                {JSON.stringify(speaker.sessions)}{" "}
                {speaker.favorite === true ? "true" : "false"}
              </div>
          );
        })}
      </>
  );
}

export default Speakers;
2 Answers

It's fine to use a shallow copy if the data is not being manipulated in anyway.

The risk of using a shallow copy only exists if the operation you are performing changes the data, as this will result in a direct mutation of the react state bypassing setState, which can cause side effects such as state not matching rendered results.

For deep cloning, you can use a library like lodash.cloneDeep to avoid having to handle the object manually.

When updating state the optimal way you would be making new copies for only the objects you update, and leave the others untouched. Unchanged pieces of your state won't have any impact by keeping the old reference, no need to copy them.

Your goal is avoid mutating pieces of the state. This way you ensure that your update state reflects correctly without unexpected behaviors. Create deep copies are not necessary, it could be costly for deep nested states.

Your first example is close to how I would approach:

  • map makes a new array, this way you won't mutate it;
  • you create a shallow copy for the object you need to change, and update the required values correctly;

The difference is the untouched object don't need to return a new copy {...rec}. Given it's untouched you can return the same object safely, avoiding the cost of a new object.


const newSpeakersDataShallow = speakersData.map(function (rec) {
  return speaker.id == rec.id
      ? { ...rec, favorite: !rec.favorite }
      : rec;
}); 
Related