React useState Functional Updates and Why They're Important
Make sure your state is never stale by using a functional update
The most common way to manage state in React is with the useState
hook.
The hook returns a tuple where the first value is the state and the second value is a function to update the state.
Here's a basic example:
function MyComponent() {
const [count, setCount] = useState(0);
const onClick = () => setCount(count + 1);
return <button onClick={onClick}>{count}</button>;
}
In this example, we're managing state for a counter component. The button displays the current count, and when you click on the button, the count increases by 1.
Easy enough!
But this isn't the only way you can update state in React. The updater function can also take another function as its argument. This function gets the current state and should return the next state.
In other words, you can set the next state based on the current state:
function MyComponent() {
const [count, setCount] = useState(0);
const onClick = () => setCount(prevCount => prevCount + 1);
return <button onClick={onClick}>{count}</button>;
}
But aren't we already doing that?
In this case, yes.
If you're updating state in an event handler both examples above should work the same way. That said, there are certain situations where the first version of the state update may not do what you expect.
Before we get to that, we need to talk about closures.
Understanding Closures
In JavaScript, a closure refers to a function and all the variables it can access. When a function is defined, it can reach variables that exist outside of it if the variable is within scope.
Here's an example:
The console.log
method can access the outside
variable because it's in the scope of both functions. However, the second function can't access the insideOne
variable because it isn't within scope.
In other words, function two closes over the outside
and insideTwo
variables, but not the insideOne
variable.
The closure refers to the function and whatever variables it can access.
Ok great! But what does this have to do with React state updates?
When React re-renders your component, new state variables are created.
One thing that might not be obvious with React components is that when a component renders, new variables are created.
Let me show you an example:
Both buttons are meant to update the count state.
The problem is that the second button keeps setting the value to 1
. This is because we only assign the updateCount
function on the initial render.
Every re-render creates a newcount
variable which is how the first button works, but the updateCount
function closes over the count
variable at the time of its creation. In other words, as far as the updateCount
function is concerned, count
is always 0
.
Great! But this example isn't really practical. You wouldn't define a function this way, so when would the functional update actually be useful?
How to update state on an interval.
This issue can pop up when you try to update state outside of an event handler, such as in a setInterval
call.
The setInterval
method takes a function as its first argument and a time in milliseconds as the second argument. It then executes the function on an interval based on the second argument. For example, if you pass 1000
as the second argument, your function will execute every second.
The key part about this is that the function you pass to setInterval
is only created once, which means if you try to update state inside of this function, the state value it closes over will be the value at the time of its creation.
If that doesn't make sense, let's look at an example.
In the example below, we have two countdown timers and a button to start the timers. Both timers update every second. The only difference between them is that the first timer uses a regular state update and the second timer uses a functional state update.
See what happens when you start the timers:
Notice how the first timer gets stuck at 59
seconds, but the second timer keeps decreasing. We're calling both state updaters in the function we passed to setInterval
, but the first updater is referencing a stale state value (i.e. timerOne
).
The second state updater works because when you use a functional update, React is guaranteed to use the latest version of the state when it calls the function.
In most cases, you should be fine using the first version to update React state, but it can be useful to understand how the second version works in case you need to update state within a closure that references a stale state value.