JavaScript React Components Managing State and Data Flow Update State Based on a Player's Index

Lean Flores
Lean Flores
25,852 Points

Is it safe to mutate prevState? i.e. prevState.players[index].score += delta

the react docs says that we should not:

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. For instance, suppose we wanted to increment a value in state by props.step:

Bimal Kharel
Bimal Kharel
346 Points

Hi, can anyone please explain,

handleScoreChange = (index, delta) => {

this.setState((prevState) => ({
  Score: prevState.players[index].score += delta
}));
}

I am not able to increase or decrease by 1, but the value appends and the result is like If the score is 0, result: 0 1 or 0 -1 -1 -1

11 Answers

Jeff Wong
Jeff Wong
10,154 Points

Hi guys,

I believe Chris Shaw's explanation is not correct here. prevState does indeed reference to the same object as this.state. To prove my point, consider the following code (feel free to follow along, you will gain a lot of insights):

handleScoreChange = (index, delta) => {
  const originalStatePlayers = this.state.players;

  this.setState((prevState) => {
    const prevStatePlayers = prevState.players;
    console.log(originalStatePlayers);
    debugger;
  }   
}

I insert a debugger entry point to play around with the variables. You can also use console.log here to see what the variables are. I included console.log(originalStatePlayers) so we won't lose originalStatePlayers value inside the setState block. Just click on one of the increment or decrement button and the call stack will stop at the debugger point. Now we can test in the browser console:

originalStatePlayers === prevStatePlayers // true

As we know, JavaScript compares object (and array because array is an object in JS) on reference, not value. It seems like originalStatePlayers and prevStatePlayers not just having the same value, but they also refers to the same location in memory. To further prove it, let's create another object with the exact same value as originalStatePlayers and prevStatePlayers:

var anotherPlayers = [{name: 'Guil', id: 1, score: 0}, {name: 'Treasure', id: 2, score: 0}, {name: 'Ashley', id: 3, score: 0}, {name: 'James', id: 4, score: 0}]

anotherPlayers === originalStatePlayers // false
anotherPlayers === prevStatePlayers // false

Now the important question is: if I mutate prevStatePlayers or anotherPlayers, will they affect originalStatePlayers? Let's dig in:

prevStatePlayers.push({name: 'George', id: 5, score: 0})

prevStatePlayers
/* [{name: 'Guil', id: 1, score: 0},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0},
    {name: 'George', id: 5, score: 0}] */

originalStatePlayers
/* [{name: 'Guil', id: 1, score: 0},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0},
    {name: 'George', id: 5, score: 0}] */

anotherPlayers
/* [{name: 'Guil', id: 1, score: 0},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0}] */

It looks like mutating prevStatePlayers has the same effect on originalStatePlayers but not on anotherPlayers. Let's try to mutate anotherPlayers:

anotherPlayers.push({name: 'Michael', id: 6, score: 0})

anotherPlayers
/* [{name: 'Guil', id: 1, score: 0},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0},
    {name: 'Michael', id: 6, score: 0}] */

prevStatePlayers
/* [{name: 'Guil', id: 1, score: 0},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0},
    {name: 'George', id: 5, score: 0}] */

originalStatePlayers
/* [{name: 'Guil', id: 1, score: 0},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0},
    {name: 'George', id: 5, score: 0}] */

Important conclusion here: mutating prevState WILL mutate the original state object. If we are to follow strict React guidelines on setting state without mutating the original state, we should not set our players state here directly with prevState.

Instead, we should create a new array from prevState (Before we continue, let's refresh the app and get ourselves a new clean originalStatePlayers and prevStatePlayers - they refer to the same object anyway. Now click on one of the increment/decrement button again to stop at the debugger point. Continue the following in the browser console):

var updatedPlayers = [...prevStatePlayers]
updatedPlayers === prevStatePlayers // false

I am using the spread operator [...] to clone a new array from prevStatePlayers array. Notice that I used var instead of const because I'm inside of my browser console and it just wouldn't show the value of the variables if I used const. In the actual code I will definitely use const to create the variables.

If we push or remove elements from our updatedPlayers array, it will not affect prevStatePlayers array. However, if we do this:

updatedPlayers[0] // {name: 'Guil', id: 1, score: 0}
prevStatePlayers[0] // {name: 'Guil', id: 1, score: 0}

updatedPlayers[0].score = 1

updatedPlayers
/* [{name: 'Guil', id: 1, score: 1},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0}] */

prevStatePlayers
/* [{name: 'Guil', id: 1, score: 1},
    {name: 'Treasure', id: 2, score: 0},
    {name: 'Ashley', id: 3, score: 0},
    {name: 'James', id: 4, score: 0}] */

"But I thought we created a new array! Jeez JavaScript is weird..." I know right? The things is, even though we cloned a new array, it is actually only a shallow copy, which means if we have objects (or arrays, because arrays are objects in JS, again) nested inside our cloned array, they still have the same reference as the original array. In other words, when we perform a shallow clone, the outer object/array is new, but the nested objects/arrays are still old.

One solution is to deep clone our object/array, but searching through the web we will find a million reasons why we shouldn't do that. The best solution from my research is to only clone the nested object/array that we want to mutate, which will have the best performance implication.

We know that we definitely need a new array to mutate, so let's change our code to this:

handleScoreChange = (index, delta) => {
  const originalStatePlayers = this.state.players;

  this.setState((prevState) => {
    const prevStatePlayers = prevState.players;
    const updatedPlayers = [...prevStatePlayers];
    console.log(originalStatePlayers);
    console.log(index);
    debugger;
  }   
}

Refresh the app, click on the increment button next to Guil, and follow along in the browser console:

index // 0 if we click on any buttons next to Guil
var updatedPlayer = {...updatedPlayers[index]}

updatedPlayer // {name: 'Guil', id: 1, score: 0}

We cloned a new object updatedPlayer from the first object in updatedPlayers array. Now if we change the score of updatedPlayer:

updatedPlayer.score = 2

updatedPlayer // {name: 'Guil', id: 1, score: 2}

updatedPlayers[0] // {name: 'Guil', id: 1, score: 0}
prevStatePlayers[0] // {name: 'Guil', id: 1, score: 0}
originalStatePlayers[0] // {name: 'Guil', id: 1, score: 0}

It looks like mutating updatedPlayer doesn't effect any of updatedPlayers, prevStatePlayers and originalStatePlayers. To assign the new changes to our new updatedPlayers array, simply do:

updatedPlayers[0] = updatedPlayer

updatedPlayers[0] // {name: 'Guil', id: 1, score: 2}
prevStatePlayers[0] // {name: 'Guil', id: 1, score: 0}
originalStatePlayers[0] // {name: 'Guil', id: 1, score: 0}

Great, we achieved what we are looking to do here - updating our state without mutating the original state. Therefore, I believe the most 'React' way to update the score is:

handleScoreChange = (index, delta) => {
  this.setState((prevState) => {
    const updatedPlayers = [...prevState.players];
    const updatedPlayer = {...updatedPlayers[index]};
    updatedPlayer.score += delta;
    updatedPlayers[index] = updatedPlayer;

    return {
      players: updatedPlayers
    };
  });
}

I don't know what the implications will be if we always mutate the state directly, as I have never encounter it myself. However, as stated in React documentation, some weird behaviour might happen and I can imagine it will be very difficult to track down. I am happy to hear if anyone encounters such issues before and share their findings as well.

Chris Shaw
Chris Shaw
26,626 Points

Hi Jeff Wong,

I understand your points but I stand by my understanding of how this.setState works. I have created a demo which backs my statements at the below link.

https://stackblitz.com/edit/react-cy4dyu?file=index.js

The demo shows that mutating prevState doesn't mutate the state object itself since it's a clone of the state at that point in time.

Chris Shaw
Chris Shaw
26,626 Points

Hi Lean Flores,

The code Guil Hernandez wrote is perfectly valid as prevState is a clone of the state object, not the original. It's actually recommended that you mutate this object as it's already in memory and doesn't require additional lookups to the component's state which is common when doing something like the below.

// Bad, mutation occurs directly on the state
this.setState({
  score: this.state.players[index].score += delta,
})

// Good, mutation occurs on the cloned object
this.setState((prevState) => ({
  score: prevState.players[index].score += delta,
}))

Hope that helps.

Lean Flores
Lean Flores
25,852 Points

Hi Chris,

When coded this way,

this.setState((prevState) => { prevState.counter += 1 return { name: "React_1" } }

counter is still updating which leads me to think that mutating prevState is mutating the actual state and not a clone of it

Chris Shaw
Chris Shaw
26,626 Points

The reason your example is mutating is because you are directly changing prevState within a function call. The example code in my link is creating a new object that is unable to mutate due to this. In the case of the function, you are going against React's recommendation by mutating the object directly.

The key takeaway here is that you should always generate a new object from the previous state and give that back to the component which is shown in my first reply and the link supplied. To satisfy your example, the code would become the following:

this.setState((prevState) => {
  return {
    counter: prevState.counter + 1,
    name: "New name"
  }
})
Jeff Wong
Jeff Wong
10,154 Points

Hi Chris Shaw,

I added one line inside your incrementCounter() function to make things clearer:

incrementCounter() {
  console.log('(Pre) Sync updated!', this.state.counter)

  this.setState((prevState) => {
    console.log('Async before update!', this.state.counter)
    return {
      counter: prevState.counter += 1,
    }
  }, (state) => {
    console.log('Async updated!', this.state.counter)
  })

  console.log('Sync updated!', this.state.counter)
}

And the results in the console:

Clearly this.setState is ran after (Pre) Sync updated and Sync updated because of JS asynchronous nature. The second callback in setState will take precedence over ANY code after state is set, thus Async updated will always be after Async before update. With your example you actually proved that mutating prevState = mutating this.state directly, which means prevState is not a clone of the state object.

Two more points to think about:

  1. If prevState is indeed a clone of the state object, this will make React inefficient and slow if a component has a huge amount of states or if they are deeply nested. Why? Because then React has to deep clone them to give us a clone prevState which will have negative implications on the performance, and I certainly don't think React is designed to do that.

  2. We wouldn't need to use any of the spread operators, Object.assign() or any other way to clone objects/arrays inside our component's state if prevState is in fact a clone of the state object, don't we?

Lean Flores
Lean Flores
25,852 Points

Even if its coded this way

this.setState((prevState) => ({ counterzzz: prevState.counter += 1, })

it is still updating. Again, leads me to think prevState is not a clone and should not be mutated (i.e. using "+=")

Chris Shaw
Chris Shaw
26,626 Points

I'm concerned that your result is different to mine, as can be seen in the screenshot below, the counter change doesn't mutate the original state and it is consistent every time the button is pushed.

React Counter Example

Lean Flores
Lean Flores
25,852 Points

I think what your demo is showing is the async nature of javascript. As evidenced by the order of the console logs, Pre > Sync > Async... the reason the Sync log is not showing the updated state is because this.setState() is not yet done procccessing while the Async log is in the callback function, therefore displaying the updated state.

Chris Shaw
Chris Shaw
26,626 Points

That's not the point I was trying to raise, you're saying that mutation on your end is causing the state to change whereas my example shows that state is untouched before setState completes its execution.

Lean Flores
Lean Flores
25,852 Points

I don't see how that proves that mutating prevState is not mutating the actual state; when you are checking the state before setState finishes. If we are looking at the proper way of implementing setState and using prevState, shouldn't we check state AFTER we are sure that setState is done to verify the effects of our implementation to the state. The last two examples I have given does not even return a state object for "counter" yet it was updated, don't you think this proves that mutating prevState mutates the state as well? Which is against the React docs' recommendation

Jeff Wong
Jeff Wong
10,154 Points

Actually, this code is much better for explaining:

incrementCounter() {
  console.log('(Pre) Sync updated!', this.state.counter)

  setTimeout(() => {
    this.setState((prevState) => {
      console.log('Async before update!', this.state.counter)
      return {
        counter: prevState.counter += 1,
      }
    }, (state) => {
      console.log('Async updated!', this.state.counter)
    })
    console.log('Everything is done! No more execution!', this.state.counter)
  }, 2000)
  console.log('Sync updated!', this.state.counter)
}
Kyle Green
Kyle Green
9,666 Points

Jeff Wong's answer is correct, the code in the video is directly mutating state which is not recommended. You can read the React documentation yourself https://reactjs.org/docs/react-component.html#setstate, and get a better picture of why https://stackoverflow.com/questions/47339643/is-it-okay-to-treat-the-prevstate-argument-of-setstates-function-as-mutable.

I think what he did is modifying the previous state ?

 this.setState( prevState => {
    // modify previous state
    return {
        anyNameWillWork : prevState.players[index].score += delta
   };
});

Without modifying previous state

this.setState( prevState => {
      let newPlayer = Object.assign({}, prevState.players[index]);   // copy at index: 3
      newPlayer.score += delta;   // update score
      return {
        players: [
          ...prevState.players.slice(0, index),  // copy from index 0 to 2
          newPlayer,  // add new at index 3
          ...prevState.players.slice(index+1) // copy the rest from index 4.
        ]
      }
});