When we might need useState functional updates and how to use them

When we need to hold any state, useState
is the most common solution in React nowadays. It returns the current state value and a method to update it:
const [state, setState] = useState(initialValue);
Most of the time, setState
is used in a straightforward way when we call it with our desired state value passed as an argument: setState(newState)
.
It’s simple and sufficient in most cases.
However, it’s not the only way to modify the state with setState
. In the React documentation about useState
, there is a paragraph explaining a functional way to update the state: functional updates.
Basically, they let you pass a function to setState
instead of a value. That function will receive the current state as the first parameter and the returned value will become a new state, like this:
setState(currentState => { const newState = modify(currentState); return newState; });
When Might I Need to Use Functional Updates?
If we are operating in a synchronous environment, most of the time, a regular setState
will be sufficient. It starts to get complicated when multiple pieces of the application share the state and we have to work with asynchronous functions (e.g. when making API requests).
In other words, when we call an asynchronous function and reach the point of calling setState
, other variables that we calculate a new state from might already be outdated.
Example
For the purpose of this article, let’s assume we’re building a simple counter. It will be incremented when buttons are clicked.
We will have two buttons: one just to increment the counter instantly and another that does that with some timeout (mimicking an API call going on, for example).

Implementation
Regular setState
Let’s start with just a simple, synchronous button to increase the counter:
import { useState } from "react";
export function FunctionalUpdates() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter(counter + 1);
}
return (
<div>
<div>Counter value: {counter}</div>
<button onClick={handleIncrement}>
Increment
</button>
</div>
);
}
It would work like this:

Adding an asynchronous call
Now, let’s add the second button that will increment the counter asynchronously. We’ll use the following wait
helper function to mimic an asynchronous function call that finishes after a specified period of time:
export function wait({ miliseconds }) {
return new Promise((resolve) => setTimeout(resolve, miliseconds));
}
Let’s make the increment function async
, wait for some timeout, and then increment the counter as we did with the first button:
import { useState } from "react";
import { wait } from "./wait";
export function FunctionalUpdates() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter(counter + 1);
}
async function handleIncrementAsync() {
await wait({ miliseconds: 2000 });
setCounter(counter + 1);
}
return (
<div>
<div>Counter value: {counter}</div>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleIncrementAsync}>Increment asynchronously</button>
</div>
);
}
We made the function asynchronous, and we might think that this should do the job. But let’s check the outcome:

We increment the counter, then hit the asynchronous one, and increment it again a couple of times. Why is the final state of the counter 2
?
When handleIncrementAsync
was called, the counter
value was 1
. This value will be remembered for the whole lifecycle of the function. That’s why we get 2
at the end. When the await wait()
finished, we still referenced the old value (1
) and incremented that.
useCallback to the help — or not?
At first, we might think that useCallback
would help to solve this situation. Putting counter
as a dependency should make the function reference the most up-to-date value. Let’s try with the following code:
import { useCallback, useState } from "react";
import { wait } from "./src/FunctionalUpdates/wait";
export function FunctionalUpdates() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter(counter + 1);
}
const handleIncrementCallback = useCallback(async () => {
await wait({ miliseconds: 2000 });
setCounter(counter + 1);
}, [counter, setCounter]);
return (
<div>
<div>Counter value: {counter}</div>
<button onClick={handleIncrement}>
Increment
</button>
<button onClick={handleIncrementCallback}>
Increment useCallback
</button>
</div>
);
}

The same situation still happens, even if counter
is in the dependency array. Whenever it changes, the handleIncrementCallback
is indeed updated, but only “new” function calls take leverage of this. If we have an already-running function (like we do since the first call to the “Increment useCallback” handler is still waiting for the wait
to finish), it doesn’t make a difference.
Functional update
Let’s explore a different way to call setCounter
now. As mentioned in the beginning, it can accept a function instead of a value. This function will be called by React and the current version of the state will be passed as an argument. This way is called a “functional update.” Let’s recap that:
setState(currentState => { const newState = modify(currentState); return newState; });
The currentState
will represent the actual value of the counter in our example. Let’s modify the code and use the functional update approach:
import { useState } from "react";
import { wait } from "./wait";
export function FunctionalUpdates() {
const [counter, setCounter] = useState(0);
function handleIncrement() {
setCounter(counter + 1);
}
async function handleIncrementFunctional() {
await wait({ milisecomds: 2000 });
setCounter(_counter => _counter + 1);
}
return (
<div>
<div>Counter value: {counter}</div>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleIncrementFunctional}>Increment functional</button>
</div>
);
}
We invoke setCounter
like this: setCounter(_counter => _counter + 1)
.
The _counter
value will represent the most up-to-date value of the counter at the time when the setCounter
is invoked. Let’s see the effect:

There are six clicks in total:
- The first one to increment asynchronously after two seconds
- Five clicks to instantiate “Increment” when we’re waiting for
wait
to resolve.
When wait
finished, the counter value was 5
. This value was passed to the functional update call. As an effect, we counted all the clicks correctly.
Little bonus
When we’re using functional updates, at first it makes sense to reuse the state variable name. For example:
const [counter, setCounter] = useState(0); // ... function handleIncrement() { setCounter(counter => counter + 1); }
But it’s not a good practice to shadow (reuse) the variable from an outer scope. We may use _
or — even better — current
to prefix the variable, but in my humble opinion, it still doesn’t look good enough:
setCounter(_counter => counter + 1); setCounter(currentCounter => currentCounter + 1);
Personally, I’d suggest just extracting the increment operation to a separate function and passing it to setCounter
:
function increment(value) { return value + 1; }
// ...
setCounter(increment);
Summary
We’ve learned a different way to update a state using functional updates. We also highlighted why a similar approach with useCallback
was not the right solution in this case.
Now, whenever you need multiple asynchronous functions to share a state, you have a solution to deal with this situation in a convenient way.
As usual, the whole project is in a GitHub repository.
Thanks for making it to the end of the article. Feel free to express your opinion in the comments. I’m very open and keen to hear your feedback, whether it’s positive or negative!