As mentioned by other members, setState runs asynchronously in a queue-based system. This means that if you want to perform a state update, this update will be piled up on the queue. However, it does not return a Promise, so you can't use a .then
or a wait
. Because of this, we need to be aware of possible problems that might occur while manipulating states in React.
Say we have:
// PROBLEM # 1
export function Button() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
console.log(count); // returns 0
}
return (
<button onClick={increment}>Increment<\button>
)
}
That is basically the same problem you have. Since setCount is asynchronous, the console.log
will return the value before the updates (which is 0 here). In your case, it will return undefined
because you didn't pass any initial state in the useState hook. Other thing you did different was to fire the state change from inside the useEffect
hook, but that doesn't matter for the problem at hand.
To fix this, a good practice is to store the new state in another variable, as follows:
// SOLUTION TO PROBLEM # 1
export function Button() {
const [count, setCount] = useState(0);
function increment() {
const newCountValue = count + 1;
setCount(newCountValue);
console.log(newCountValue); // returns 1
}
return (
<button onClick={increment}>Increment<\button>
)
}
This basically answers your question.
But, there are still other scenarios where state updates might not behave as one would expect. It is important to be aware of these scenarios to avoid possible bugs. For example, let's assume that we are updating a given state multiple times in a row, like:
// PROBLEM # 2
export function Button() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
console.log(count); // returns 0, but the final state will be 1 (not 3)
}
return (
<button onClick={increment}>Increment<\button>
)
}
Here, console.log
still returns 0 for the same reason described in Problem #1. But, if you are using the count
state somewhere in your app, you will see that the final value after the onClick
event is 1, not 3.
Here is how you can fix this:
// SOLUTION TO PROBLEM # 2
export function Button() {
const [count, setCount] = useState(0);
function increment() {
setCount((oldState) => oldState + 1);
setCount((oldState) => oldState + 1);
setCount((oldState) => oldState + 1);
console.log(count); // returns 0, but the final state will be 3
}
return (
<button onClick={increment}>Increment<\button>
)
}
By passing a function to setState, the new state is computed using the previous value on the queue and not the initial one.
As a final example, say we are calling a function right after trying to mutate our state, but this function depends on the state value. Since the operation is asynchronous, that function will use the "old" state value, just like in Problem #1:
// PROBLEM # 3
export function Button() {
const [count, setCount] = useState(0);
onCountChange() {
console.log(count); // returns 0;
}
function increment() {
setCount(count + 1);
onCountChange();
}
return (
<button onClick={increment}>Increment<\button>
)
}
Notice in this problem that we have a function onCountChange
that depends on an external variable (count
). By external I mean that this variable was not declared inside the scope of onCountChange
. They are actually siblings (both lie inside the Button
scope). Now, if you are familiar with the concept of "pure functions", "idempotent" and "side effects", you will realize that what we have here is a side-effect issue -- we want something to happen after a state change.
Roughly speaking, whenever you need to handle some side-effects in React, this is done via the useEffect hook. So, to fix our 3rd problem, we can simply do:
// SOLUTION TO PROBLEM # 3
export function Button() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count); // returns 0;
}, [count]);
function increment() {
setCount(count + 1);
}
return (
<button onClick={increment}>Increment<\button>
)
}
In the solution above, we moved the content of the onCountChange
to useEffect and we added count
as a dependency, so whenever it changes, the effect will be triggered.