React useState Functional Updates and Why They're Important

React useState Functional Updates and Why They're Important

Make sure your state is never stale by using a functional update

·

4 min read

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.