There are a lot of concepts when it comes to React. State management is one of those which is most widely used and it is a way to hold, process and describe our UI in terms of data. The state is nothing more than a JavaScript object which we can update as per user actions, API responses or any sort of trigger.
With the introduction of React Hooks, we can use state in functional components too. However, in this blog post, we are going to focus on Class-based components and understand how to use setState and their behaviour. Moreover, some of the mentioned behaviours hold for React < 17.
class App extends Component {
constructor(props) {
super(props);
this.state = {}; // Initializing state
}
}
Updating state
The state can be directly mutated via this.state
. However, it will not cause a re-render and will eventually lead to state inconsistency. You can check this behaviour using this codesandbox.
Therefore, it is recommended to use setState
to perform any state updates. For the rest of the blog post, we are going to use the following example as our base.
class App extends Component {
state = { items: [], counter: 0 };
handleClick = () => {
// some processing
};
render() {
const { items } = this.state;
console.log("Rendering", items);
return (
<div className="App">
{items.length ? (
<h2>Items are {JSON.stringify(items)}</h2>
) : (
<Fragment>
<p>No items found</p>
<button onClick={this.handleClick}>Add items</button>
</Fragment>
)}
</div>
);
}
}
Different setState function signatures
setState
can be used in two different ways
- Passing an object as an argument
...
handleClick = () => {
const { items } = this.state;
this.setState({
items: [
...items,
"apple"
]
});
}
...
In this case, we are passing an object to the setState
function which contains a part of the overall state which we want to update. React takes this value and merges it into the final state. Like, in the above code snippet, we are appending an item apple
to our items array in the state. After the merging by React, our final state would look like the following
{
"items": ["apple"],
"counter": 0
}
React's state merging is shallow. Hence, it has no impact on the counter
variable in the state.
- Passing function as an argument
...
handleClick = () => {
this.setState((state, props) => {
const { items } = state;
return {
items: [
...items,
"apple"
]
}
});
}
...
In this case, we are passing a function to the setState
. This function will receive the previous state as the first argument, and the props at that time the update is applied as the second argument. When we talk about the previous state here then it means the most up-to-date state then (we will see how this is helpful later). It returns an object which would be merged into the final state by React.
Usecases
Now, we are going to observe the behaviour of setState
invocation in different circumstances and try to understand why it is behaving in that way.
1. Multiple state updates in one scope
In our earlier code snippets, we were appending item(s) to our items array in the state. Suppose, we have a case where we want to make successive setState calls in one function scope. You can use this codesandbox to follow along.
...
handleClick = () => {
const { items } = this.state;
this.setState({
items: [...items, 'apple']
});
this.setState({
items: [...items, 'mango']
});
this.setState({
items: [...items, 'orange']
});
this.setState({
items: [...items, 'pear']
});
};
...
Open the above-mentioned codesandbox link and check the console.
- We will see that the initial output would be
Rendering []
.
- Once we click the button then the output would be
Rendering ["pear"]
.
Why does this happen?
Turns out React understands the execution context (important to note) and batches the setState
calls as per that. No matter how many successive setState
calls we make in a React event handler, it will only produce a single re-render at the end of the event and only last, as the order is preserved, reflects the state.
2. Multiple state updates in one scope using function argument signature
As we have seen above, only the last setState
call is reflected. However, we can rectify this using the function argument signature of setState
rather than object-based. You can use this codesandbox to follow along.
handleClick = () => {
this.setState((state) => ({
items: [...state.items, "apple"],
}));
this.setState((state) => {
console.log(state);
return {
items: [...state.items, "mango"],
};
});
this.setState((state) => ({
items: [...state.items, "orange"],
}));
this.setState((state) => ({
items: [...state.items, "pear"],
}));
};
In this approach, React queues up the callbacks provided to different setState
calls and each callback receives the most up-to-date state at that moment as their execution is synchronous. This is confirmed by the following code snippet
handleClick = () => {
this.setState(state => ({
items: [...state.items, 'apple']
}));
this.setState(state => {
console.log(state); // { "items": ["apple"] }
return {
items: [...state.items, 'mango']
};
});
...
};
As you can see the state
received by the second setState
callback already has the item apple
in the items array. Since we are still in the React event handler, all the setState
calls will cause a single re-render.
3. Multiple state updates in lifecycle methods
As we have seen above React batches setState
calls. However, in current versions (< 17), this is only true inside event handlers and lifecycle methods. You can use this codesandbox to follow along.
...
componentDidMount() {
const data = ["apple", "orange", "mango", "pear"];
this.setState(state => {
return {
items: { ...state.items, 1: { id: 1, value: data[0] } }
};
});
this.setState(state => {
return {
items: { ...state.items, 2: { id: 2, value: data[1] } }
};
});
this.setState(state => {
return {
items: { ...state.items, 3: { id: 3, value: data[2] } }
};
});
this.setState(state => {
return {
items: { ...state.items, 4: { id: 4, value: data[3] } }
};
});
}
render() {
const { items } = this.state;
console.log("Rendering...", items);
...
There is only one re-render despite multiple state updates. It only happens in React controlled synthetic event handlers.
4. Multiple state updates in AJAX, promises, and setTimeouts
React understands the execution context. No matter where your AJAX, promise or setTimeout is happening as in inside lifecycle methods or event handlers, React will not batch leading to multiple re-renders. You can use this codesandbox to follow along.
...
handleClick = () => {
this.setState(state => ({
items: [...state.items, "single"]
}));
this.setState(state => ({
items: [...state.items, "render"]
}));
return fakeAPI().then(() => {
this.setState(state => ({
items: [...state.items, "apple"]
}));
this.setState(state => ({
items: [...state.items, "mango"]
}));
this.setState(state => ({
items: [...state.items, "orange"]
}));
this.setState(state => ({
items: [...state.items, "pear"]
}));
});
};
...
As we can see the first two setState
calls causes a single re-render. However, calls inside the promise lead to multiple intermediate re-renders.
As per the React team, there might be more uniform behaviour from React version 17 where batching of setState will happen regardless of the execution context.