Today, I am going to share with you 3 scenarios we may encounter while updating react states
. I have often seen these common mistakes being made by some beginner React devs. I'll also show you how to avoid those mistakes.
So let's get started.
Case 1: Reading state just after setState()
Have you ever tried checking the state just after setState()
. If you haven't, let me get my hands dirty for you.
Here we have a count state which can be incremented using a button.
export default class App extends Component {
state = {
count: 0
}
handleClick = () => {
this.setState({
count: this.state.count+1
})
console.log(this.state.count)
}
render() {
return (
<div className="App">
<h1>{this.state.count}</h1>
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
Here is the output -
Check the console.
So why aren't we getting the updated state in console ?
Well the reason is that the calls to setState
are asynchronous.
So by calling setState(), we are making a request to update the state and meanwhile moving to the next line. Then the state is logged in console before the update request is completed.
Therefore, it isn't recommended to access
this.state
right after calling setState().
How to avoid -
- If you want to access the state just after setState, you may do so inside the lifecycle method - componentDidUpdate() or useEffect for functional components .
- You can also achieve this using a callback function inside the setState function. Do note that this methd won't work for setter function of useState hook.
Thanks to Geordy James for reminding this method.
Case 2: Updating object
or array
states the wrong way
Let's try to update an object state.
The following code is taking input of first name and last name and updating the states of fistName and lastName using two respective functions, but something strange is happening.
export default class App extends Component {
state = {
name: {
firstName: "",
lastName: ""
}
};
addFirstName = e => {
this.setState({
name: {
firstName: e.target.value
}
});
};
addLastName = e => {
this.setState({
name: {
lastName: e.target.value
}
});
};
resetName = () => {
this.setState({
name: {
firstName: "",
lastName: ""
}
});
};
render() {
return (
<div className="App">
First Name:
<input value={this.state.name.firstName} onChange=
{this.addFirstName} />
<br />
<br />
Last Name:
<input value={this.state.name.lastName} onChange=
{this.addLastName} />
<br />
<br />
<button onClick={this.resetName}>Reset</button>
<h1>{`Your name is ${this.state.name.firstName} ${
this.state.name.lastName}`}</h1>
</div>
);
}
}
So when you are entering the first name, the last name is undefined and vice-versa.
This is happening due to something called shallow merge.
When you update state by passing an object inside setState(), the state is updated by shallow merging. Shallow merging is a concept in javascript,using which if two objects are merged, the properties with same keys are overwritten by value of the same keys of second object.
So in our case, when we are updating first name, setState is overwriting the complete name object with the new object passed in the setState, which has either firstName or lastName.
How to Avoid -
- Use spread operator(...) - Just use spread operator to make a copy of the state and then update the state.
addFirstName = e => {
this.setState({
name: {
...this.state.name,
firstName: e.target.value
}
});
};
addLastName = e => {
this.setState({
name: {
...this.state.name,
lastName: e.target.value
}
});
};
Note - This case also applies to array
states.
Case 3: Updating state multiple times consecutively
Imagine we want to update the state multiple times in a row. We may try in the following manner.
Here we are incrementing count by 10
import React, { Component } from "react";
import "./styles.css";
export default class App extends Component {
state = {
count: 0
};
handleClick = () => {
for (let i = 0; i < 10; i++) {
this.setState({
count: this.state.count + 1
});
}
};
render() {
return (
<div className="App">
<h1>{this.state.count}</h1>
<button onClick={this.handleClick}>Add 10</button>
<button onClick={() => this.setState({ count: 0 })}>Reset</button>
</div>
);
}
}
So, instead of incrementing by 10, it is only incrementing by 1.
Well, here is the reason.
In this case, the multiple update calls are being batched together. Therefore the last call is overriding the previous calls and count is only incremented by 1.
How to avoid -
- Using updater function in setState() - One of the arguments which setState accepts is an updater function.
handleClick = () => {
for(let i = 0;i<10;i++) {
this.setState((prevState) => {
return {
count: prevState.count + 1
}
})
}
};
This way all our updates are chained and updation occurs consecutively just as we wanted, instead of calls overriding each other.
So whenever your new state value depends on the current state value, use the updater function which will always give you the current updated state.
Closing Note -
- All these cases remain same for
useState()
hook since the setter function of useState hook issetState()
only. - Currently, setState calls are only batched inside event handlers but in the upcoming
React v17
, this will be the default behaviour. - If you want to know more about why setState is Asynchronous or why calls to setState are batched, read this in-depth comment.
- This is my first blog ever and I would gladly welcome any suggestions and feedback π€.
Top comments (5)
The second parameter of setState is a callback. So this will work too...
Yup this works too. Thnx for reminding.
last Case 3: You showed the Class example, where is the Hooks example ?
Nice tutorial but if I remember this well you copied copied the tutorials from this channel:
youtube.com/playlist?list=PLC3y8-r...
Hey I did write this blog after clearing all my doubts from official react docs, stack overflow and yeah I did watch one of his videos in the process. I felt his example made the problem clear so took the inspiration for that code from the video. That's all.