Does React create a deep or shallow copy of prevState internally?

Viewed 668

I'm just starting with programming and have recently written my first app in React, and while it does work, I'm not sure if I'm handling my internal state correctly. My question is whether the setState method creates an immutable deep copy of the "prevState" or not necessarily? To show an example:

 menuAddRecHandler = () => {
    this.setState((prevState) => {
      const updatedState = {...prevState};
      const dayInd = updatedState.menu.recipes.findIndex(item => item.dayNum === updatedState.menuInternal.currentDay);
      updatedState.menu.recipes[dayInd].recList.push({
        name: prevState.menuInternal.menuRecSelect,
        portions: prevState.menuInternal.menuNumPortInput
      });
      return updatedState;
    })
  }

In my handler I am pushing an object to a nested array of the updatedState object which was copied over from prevState by the spread operator. Now I know that the spread operator makes only a shallow copy, but is the prevState supplied internally by the setState method also shallow, and does that mean that I am actually mutating my state directly by invoking this handler? If so, how do I fix this issue? Thank you.

3 Answers

From the docs:

The first argument is an updater function with the signature:

(state, props) => stateChange

state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props.

How can you achieve the same result without mutating?

menuAddRecHandler = () => {
  this.setState((prevState) => {
    return {
      ...prevState,
      menu: {
        ...prevState.menu,
        recipes: prevState.menu.recipes.map((item) => {
          if(item.dayNum === prevState.menuInternal.currentDay) {
            return {
              ...item,
              recList: [
                ...item.recList,
                {
                  name: prevState.menuInternal.menuRecSelect,
                  portions: prevState.menuInternal.menuNumPortInput
                }
              ]
            }
          }
          return item;
        })
      }
    }
  });
}

This is obviously very error prone, the idea is to have less deeper states. The docs for Redux offer some tips for normalizing state.

You can also look at immutable state libraries like ImmutableJS or Immer

According to the docs its's not a copy at all

state is a reference to the component state at the time the change is being applied. It should not be directly mutated.

Making a deep copy is usually bad for performance, but if you have to do it, you can do something like this:

const updatedState = JSON.parse(JSON.stringify(prevState));

There is a lot of "noise" in your example so I'll keep it simple:

state = {  name: `Dennis`, age: 89 }

this.setState((prevState) => {
  return { age: prevState.age + 1 }; 
}); // { name: `Dennis`, age: 90 }

this.setState({ name: `John` }); // { name: `John`, age: 89 }

In class component, this.setState merges shallowly with the previous state, and the prevState is the actual state reference i.e: { name: "Dennis", age: 89 }.

Both state and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with state.

Note that it is not the case with functional components (a common mistake).

Unlike the setState method found in class components, useState does not automatically merge update objects.

Related