Why functional state updates are important
Today, we're going to look at why functional state updates are important in React, talk about when you should use one, and discuss some of the issues you may encounter if you don't.
Table of contents
- What is a functional state update?
- When should I use one?
- What if I don't use one?
- Examples
- Something to think about
- Conclusion
What is a functional state update?
A functional state update is one in which you use the callback from setState
to retrieve the most up-to-date state value and looks something like this:
1setCount((previousCount) => previousCount + 1);
The setter function from the useState
hook acts asynchronously, meaning that if you change a piece of state and immediately try to log it from within the same closure, you can get unwanted behaviour.
You have probably come across this before and it looks something like this:
1const [count, setCount] = useState(0);23const handleClick = () => {4 setCount(count + 1);56 console.log(count); // 07};
Note: Functional state updates don't change the behavior in the example above. It is just a demonstration of how
setState
can act asynchronously.
Note how by referencing count
here, we get a stale value.
This demonstrates how setState
acts asynchronously and how it can be dangerous to access state values like this.
The setState
callback gives us a reliable, stable reference to the current (also known as previous) state.
When should I use one?
You should consider using a functional state update when you rely on existing state to make an update.
Let's look at an example where we should use a functional state update.
1const [count, setCount] = useState(0);23const handleClick = () => {4 setCount(count + 1); // We are relying on previous state.5};
Note how we are computing our new state value based on the current/previous state value. This is an example of where we should use a functional state update to ensure a reliable reference to our count
(more on this later).
Now, here is an example where you do not need to use a functional update as the previous state value is irrelevant.
1const [name, setName] = useState("Louis");23const handleClick = () => {4 setName("John"); // We are not relying on previous state.5};
What if I don't use one?
If you make updates to state when relying on previous state values without using a functional state update, you can introduce race conditions and silent bugs into your application.
You can often run into issues when state is updated again before a render happens.
This can result in stale state which we'll demonstrate below.
Examples
Let's refer to the previous counter example and introduce a bug by not using a functional state update.
1const [count, setCount] = useState(0);23const handleClick = () => {4 setCount(count + 1);56 setCount(count + 1);7};
Note: Please remember that this is a contrived example solely for the purposes of this article.
Here you can see that we update state again before a render happens without using a functional update which results in a stale reference to count
for our second update.
When we click the button to increment our count, it only increments by one:
We could resolve this by using functional state updates:
1const [count, setCount] = useState(0);23const handleClick = () => {4 setCount((previousCount) => previousCount + 1);56 setCount((previousCount) => previousCount + 1);7};
Now, as we use the functional update, we refer to the up-to-date state value and our button increments the count by two:
Something to think about
If you find yourself using multiple setState
updates and have multiple pieces of state that rely on each other, consider combining them by using a reducer.
Conclusion
The examples I used are very low level and highly contrived, but hopefully they convey the concepts we covered clearly and make them easy to understand.
Hopefully you enjoyed this article and you learnt a thing or two about state updates.