Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 17

Updating State with Callbacks

Recommended resources

Hello! Welcome back.

In our last lesson, we introduced the useState hook, exploring how to add state to components and how calling the setter function triggers a re-render. We ended by noting that while an update like setCount(count + 1) works for simple cases, it has a subtle but significant flaw.

Today, we'll address that flaw head-on. This lesson is dedicated to learning the robust and recommended pattern for updating state when the new value depends on the previous one. You'll learn how to pass a callback function to your state setter, a technique that prevents common bugs related to asynchronous updates and React's performance optimizations.


1. The Problem with Stale State

Let's revisit our counter example. Imagine we want a button that increments the count by three. A logical first attempt might be to call the setCount function three times in the event handler:

function Counter() {
  const [count, setCount] = useState(0);

  function incrementByThree() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  return (
    <>
      <h2>{count}</h2>
      <button onClick={incrementByThree}>+3</button>
    </>
  );
}

You would expect the count to increase by three on each click. However, if you run this code, you'll find it only increments by one.

Why? This goes back to the concept from our last lesson: state is a snapshot. When the incrementByThree function is executed, the value of count is fixed for that entire render cycle. If count is 0 when you click the button:

  • The first setCount(count + 1) call becomes setCount(0 + 1).
  • The second setCount(count + 1) call also becomes setCount(0 + 1).
  • The third call is the same: setCount(0 + 1).

React batches these state updates for performance, and since all three calls are requesting to set the state to 1, the final result is 1, not 3. Each call is working with a stale value of the count state.

This video demonstrates a similar scenario very effectively.

React Hooks Tutorial - 3 - useState with previous state

The video 'React Hooks Tutorial - 3 - useState with previous state' from Codevolution provides a clear, practical example of this stale state problem.

Watch from the beginning to 04:07. The presenter first builds a simple counter and then creates an 'Increment 5' function using a loop. Notice how, despite calling setCount five times, the count only increases by one, perfectly illustrating the issue of stale state.



2. The Solution: The Updater Function

To solve this, React provides a second way to call your state setter. Instead of passing the new value directly, you can pass a function. This is often called an "updater function" or a "callback function".

This function receives the most recent, up-to-date state value as its argument and should return the new state value.

The syntax looks like this:

setCount(previousCount => previousCount + 1);

Here, previousCount (you can name it anything, but prev or prevState is common) is guaranteed by React to be the current state value, even if other updates are pending in the same batch.

Let's fix our incrementByThree function using this pattern:

function incrementByThree() {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
}

Now, when React processes this batch:

  1. It calls the first function with the current state (e.g., 0), which returns 1.
  2. It calls the second function with the result of the first update (1), which returns 2.
  3. It calls the third function with the result of the second update (2), which returns 3.

The final state becomes 3, just as we intended.

Direct vs. Functional State Updates with useState
This image visually summarizes the problem and solution. Calling `setCount(count + 1)` multiple times results in an incorrect update because each call references the same initial state. In contrast, the functional update `setCount(prev => prev + 1)` chains the updates correctly, as each function receives the result of the previous one.

The official React documentation is the best place to read about this pattern.

useState - React

Let's turn to the official React documentation for a definitive explanation. This resource is essential for any React developer.

In the linked page, find and read the section titled 'Updating state based on the previous state'. It provides a clear, concise example and explains exactly how React queues updater functions to ensure correctness.

Now, let's see the fix applied in the video we started with.

React Hooks Tutorial - 3 - useState with previous state

The Codevolution video demonstrates the solution perfectly.

Continue watching from 04:07 to the end (05:36). The presenter refactors the 'Increment 5' function and the other counter buttons to use the updater function pattern, showing how it solves the problem and is the safer approach.



3. A Deeper Dive: Why Updater Functions are Essential

As someone with a coding background, you understand that language features often exist to solve specific underlying problems. The updater function is a solution to two related concepts: state batching and stale closures.

State Batching Explained

As we've discussed, React groups multiple state updates from a single event (like a click) into one single re-render. This is a crucial performance optimization that prevents your UI from re-rendering unnecessarily multiple times.

The following article gives a fantastic step-by-step breakdown of how React handles these batches.

React State Deep Dive: Basics, Gotchas & Patterns - DEV Community

The article 'React State Deep Dive' provides a clear, conceptual walkthrough of why direct updates fail during batching and why functional updates succeed.

Read the subsections under 'Batching & “are state updates synchronous?”', specifically 'Direct value form — why it “fails” here' and 'Updater function form — why it works'. The visual flow (Render 1 starts...) is particularly helpful for understanding the mechanics.

Stale Closures

This issue also appears in asynchronous operations, like a setTimeout. A function created inside a component "closes over" the props and state of the render in which it was created. If that state changes later, the function still holds a reference to the old, stale value. The updater function pattern elegantly solves this by not depending on a closed-over variable.

React State Management with Functional Updates Example
This code shows a `BankAccount` component. Notice that both the immediate deposit and the scheduled deposit (inside `setTimeout`) use the functional update `setBalance(prevBalance => ...)`. This ensures that even the delayed update operates on the most current balance, avoiding bugs from stale closures.


4. Applying the Pattern to Objects and Arrays

This principle isn't just for numbers. It's equally, if not more, important when working with objects and arrays in state. Remember, you must never mutate state directly; you must always replace it. The updater function is the safest way to create a new object or array based on the previous one.

// GOOD: Using an updater function to add an item to an array
setItems(prevItems => [...prevItems, newItem]);

// GOOD: Using an updater to update a property in an object
setUser(prevUser => ({ ...prevUser, name: 'New Name' }));

This final video provides an excellent summary of everything we've covered and extends the concept to arrays and objects.

Avoid this React State Mistake | React Previous State Explained

Dave Gray's video 'Avoid this React State Mistake' recaps the concept and shows how it applies to more complex state.

Watch from 02:08 to 12:28. The first part (until ~07:21) reinforces the counter example. The second part is crucial as it demonstrates how to apply the same prevState pattern when updating arrays and objects, which is a very common task in React applications.



Conclusion

You've now mastered a critical pattern for robust state management in React. While setCount(count + 1) might seem simpler, understanding when and why to use the updater function will save you from hard-to-debug issues down the line.

Key Takeaways:

  • Rule of Thumb: If your new state depends on the previous state, always use the updater function form: setValue(prevState => ...).
  • This pattern solves issues caused by React's state batching, where multiple updates in one event handler can reference stale state.
  • The updater function receives the most up-to-date state as its argument, guaranteeing your update is based on the correct value.
  • This principle is universal and applies to all types of state, including numbers, strings, arrays, and objects.

Next Up:

We've now covered the core mechanics of initializing and safely updating a single piece of state. In the next lesson, we'll expand on this by looking at how to manage multiple, independent state variables within a single component, and discuss the trade-offs of grouping related state into an object versus keeping it separate.