I know this.state
is not supposed to be modified directly, instead setState
should be used.
From this I inferred that prevState
should be also treated as immutable and instead setState
should always look something like this:
this.setState((prevState) => {
// Create a new object instance to return
const state = { ...prevState };
state.counter = state.counter + 1;
return state;
});
Or with deeper nesting:
this.setState((prevState) => {
// Create a new object instance to return
const state = { ...prevState, counter: { ...prevState.counter } };
state.counter.value = state.counter.value + 1;
return state;
});
Or just a partial update like would be with setState({})
where easier and nicer to use:
this.setState((prevState) => ({ counter: prevState.counter + 1 }));
All of the above are obviously correct because they return a new instance, but then I came across this question where the accepted answer encourages mutating prevState
without returning a new object instance (notice the code block in the question).
Something like this:
this.setState((prevState) => {
prevState.flag = !prevState.flag;
return prevState;
});
I found this to be a sketchy suggestion so I decided to test if the object instance references of prevState
and this.state
are the same:
(The JSFiddle)
class Test extends React.Component {
state = { counter: 0 };
onDemonstrateButtonClick = (event) => {
this.setState((prevState) => {
if (prevState === this.state) alert(`uh, yep`);
prevState.counter++;
return prevState;
});
};
render() {
return (
<div>
<button onClick={this.onDemonstrateButtonClick}>Demonstrate</button>
{this.state.counter}
</div>
);
}
}
Whadayaknow, they are! So which is it? Is the answer wrong and should I return a new object instance or return partial update as a new object or can I go wild and mutate the prevState
argument directly? And if yes, how is this any different from mutating this.state
directly?
Side note: TypeScript React typings do not mark the argument as ReadOnly
which only adds to my confusion.
Both are fine, the one could just be considered a "better practice" than the other. Both signatures can be used, the only difference is that if you need to change your state based on the previous state you should use this.setState (function) which will provide you a snapshot ( prevState) from the previous state.
Don't do prevState.counter++ as this mutates prevState. Do prevState.counter + 1 instead. It does. 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.
Let me start by defining prevState... prevState is a name that you have given to the argument passed to setState callback function.
prevState is a name that you have given to the argument passed to setState callback function. What it holds is the value of state before the setState was triggered by React; Since setState does batching, its sometimes important to know what the previous state was when you want to update the new state based on the previous state value.
Is it okay to treat the prevState argument of setState's function as mutable?
The answer is NO you should never mutate prevState
, this is also clearly mentioned in react documentation in setState section
prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props.
You tested prevState
and this.state
and they are the same, well actually they are not.
To figure out why they are actually different we need to know why prevState
actually exist, and the answer is that setState
function is asynchronous, thats why react js is giving us access to prevState
lets check the example below where prevState != this.state
In the example below we will increment counter twice per click, but we will use 2 setState
operations each one of them will increment the counter by 1.
Because setState
is async
you will notice that the second setState
operation started before the first setState
is finished this is where prevState
is useful and here prevState
and this.state
are not equal.
I commented each line with a number denoting when this line is executed, this should explain why we need prevState
and why it is different from this.state
.
class App extends React.Component{
constructor(props)
{
super(props);
this.state = {
counter : 1
};
}
increment = () =>{
this.setState((prevState , props) => {
console.log("in first"); //3
console.log(prevState); //3
console.log(this.state); //3
if(prevState == this.state)
{
console.log("in first prevState == this.state");
}
return {
counter : prevState.counter+1
}
} , ()=>{
console.log("done updating first"); //5
});
console.log("after first"); //1
this.setState((prevState, props) => {
console.log("in second"); //4
console.log(this.state); //4
console.log(prevState); //4
if (prevState == this.state) {
console.log("in second prevState == this.state");
}
return {
counter: prevState.counter + 1
}
} , () =>{
console.log("done updating second"); //6
});
console.log("after second"); //2
}
render(){
return (
<div>
<span>{this.state.counter}</span>
<br/>
<button onClick={this.increment} >increment</button>
</div>
)
}
}
The Result from the above code is
"after first"
"after second"
"in first"
▶Object {counter: 1}
▶Object {counter: 1}
"in first prevState == this.state"
"in second"
▶Object {counter: 1}
▶Object {counter: 2}
"done updating first"
"done updating second"
The above code is fully working in this link, you can check the console.log result https://codesandbox.io/s/k325l485mr
The above example will correctly increment counter twice per click, if you want to break it change return statement in second setState
from
return {
counter: prevState.counter + 1
}
to
return {
counter: this.state.counter + 1
}
and you will find that the result is not correct each click will result in 1 increment which is not correct because we have 2 setState
, this is because we didn't use prevState
and we used an incorrect this.state
I believe that the correct way to update the counter is
this.setState((prevState) => ({ counter: prevState.counter + 1 }));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With