Welcome to the Treehouse Community
Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.
Looking to learn something new?
Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.
Start your free trialLean Flores
34,939 PointsIs 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:
14 Answers
Jeff Wong
10,166 PointsHi 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
26,676 PointsHi 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.
Trevor Maltbie
Full Stack JavaScript Techdegree Graduate 17,021 PointsThis is the craziest thread I've ever read on treehouse and I've never felt more scared to death of continuing on with programming.
Chris Shaw
26,676 PointsHi 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
34,939 PointsHi 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
26,676 PointsThe 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
10,166 PointsHi 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:
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 cloneprevState
which will have negative implications on the performance, and I certainly don't think React is designed to do that.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 ifprevState
is in fact a clone of the state object, don't we?
Kyle Green
9,667 PointsJeff 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.
Lean Flores
34,939 PointsEven 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
26,676 PointsI'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.
Lean Flores
34,939 PointsI 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
26,676 PointsThat'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
34,939 PointsI 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
10,166 PointsActually, 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)
}
Tola Veng
23,749 PointsI 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.
]
}
});
arieloo
13,257 PointsHi there ! The problem with the code below is that it is not updating anything in our Players array, it is creating a new object named Score.
handleScoreChange = (index, delta) => {
this.setState( prevState => ({
score: prevState.players[index].score += delta
}));
}
if you console.log state, it returns :
{players : [...], score : 1}
The Score does update to the DOM for a mysterious reason, but it's a bug !
It is not safe at all, the data we want to update is within the players array, the best way to do such is to create a new array replacing the player object with an updated score into a copy of Players, and then update State with this new Players array, my code below :
// this function sets new state
setNewState = (newState) => {
this.setState(newState);
};
// this function creates a new "players" object with updated score changes, and passes it to setNewState()
handleScoreChange = (index, score) => {
const player = this.state.players[index];
const playerUpdate = { ...player };
playerUpdate.score = score;
const newPlayers = [...this.state.players];
newPlayers[index] = playerUpdate;
this.setNewState({ players: newPlayers });
};
arieloo
13,257 PointsBEWARE MEMOIZATION !! We need a course on that topic by the way..
Motoki Higa
14,111 PointsI'm reading all the comments here, and just noticed Jeff Wong's code is different from Guil's.
I'm still trying to get my head around on this with my limited knowledge of JS, but I guess Guil's code is totally valid? Because his code is not directly mutating the prevState, instead it's returning a new object as the function is wrapped with ().
handleScoreChange = (index, delta) => {
this.setState( prevState => ({
score: prevState.players[index].score += delta
}));
}
Also in MDN, they say "Parenthesize the body of a function to return an object literal expression" Link: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
I noticed in Jeff's example,
handleScoreChange = (index, delta) => {
const originalStatePlayers = this.state.players;
this.setState((prevState) => {
const prevStatePlayers = prevState.players;
console.log(originalStatePlayers);
debugger;
}
}
Above is not returning an object like Guil's. I might be totally wrong and lacking some important knowledge about this. So if that's the case, I would like to hear what I am missing..
Cheers
Bimal Kharel
346 PointsBimal Kharel
346 PointsHi, can anyone please explain,
handleScoreChange = (index, 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