React State Updates - Sync and Async

Introduction

Have you ever wondered how React keeps track of state updates, especially when you're rapidly clicking buttons to change a counter? Before getting into the code examples, let us demystifying the magic behind the scenes, using a simple analogy.

The Magical Counter

Imagine you're in charge of a magical counter, and every time someone says a special word, the count increases. But, here's the catch – React, your trusty assistant, helps manage the counting process.

Batching Updates

React is clever; it doesn't immediately change the count every time someone utters the magic word. Instead, it takes note of all the requests to change the count and applies them together during a special "update time." This batching mechanism makes the counting process efficient, handling multiple requests at once.

The Callback Function

Now, let's introduce the callback function. It's like having a reliable friend (React) ensuring you always use the latest count when making changes. The callback function approach is akin to your friend providing the current count and updating it based on the most recent information.

Async Callbacks and Smart React

You might wonder what happens if one callback takes longer to complete. Indeed, React is smart enough to handle this. Even if the first callback takes time, React ensures that the second callback considers the most up-to-date count. This ensures a smooth and efficient process, preventing unnecessary renders.

Guess the output!!! Same component with Sync and Async setCounts

//Synchronous State Update
import { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(1);
  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }; 

  return (
    <div>
      <h1>Counter</h1>
      <p>Current count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <button>Decrement</button>
    </div>
  );
};

export default Counter;
//Asynchronous State Update
import { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(1);
  const handleClick = () => {
    setCount((prevCount)=>prevCount+1);
    setCount((prevCount)=>prevCount+1);
    setCount((prevCount)=>prevCount+1);
    setCount((prevCount)=>prevCount+1);

// Could have also been written as
    //setCount((count)=>count+1);
    //setCount((count)=>count+1);
    //setCount((count)=>count+1);
    //setCount((count)=>count+1);
// But I wanted to avoid the confusion with the same variable name count
  }; 

  return (
    <div>
      <h1>Counter</h1>
      <p>Current count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      <button>Decrement</button>
    </div>
  );
};

export default Counter;

In the first approach, setCount(count + 1) is essentially saying "increase the count by 1 based on the current value of count." Since all 4 calls are using the same value of count, the result is the same as calling setCount(count + 1) in the final call. To visualize this, think of a time you may have pressed a button in your TV remote a thousand times at once but you notice that the request was taken only once.

In the second approach, you're directly using the current count value to calculate the next state. However, React does not guarantee that state updates are applied immediately. When you call setCount 4 times like this, React may batch the updates and apply them together in the next render. To visualize this, think of a small restaurant where the chef takes direct orders from the table. You shout "A tea please". Then you correct it by shouting "Sorry, make it 2. "Also make one sugarless and one with sugar". Upon forgetting something to go with the tea, you shout "I would like some cookies to go with it". When you receive your order, you find 2 cups tea with some unsweetened cookies. In a React environment, there would have been a waiter taking down each call into his tablet and finally updating the chef after confirmation.

To ensure that you're always updating the state based on the previous state, it's recommended to use the functional form of setCount, as in the later approach. This ensures that React handles the state updates correctly, especially in scenarios where state updates are asynchronous.

Ambiguity for some

Isn't this contradicting? Ideally, when an async callback function was called, it could take any duration to complete. So, when the next callback function is called, the counter may not even know what is the most recent count. In this case, isn't it possible that all the 4 callbacks(in the second approach) may have lead to printing the same result?

Great thought, if you thought so..

In React, when you use the callback function with setCount, it's like telling React, "Hey, I want to update the count, but I want to make sure I'm using the latest count when I do it."

Now, if you call setCount twice in quick succession with a callback, React is smart enough to ensure that it takes the latest count into consideration for each update. Even if the first callback takes some time, React makes sure the second callback uses the most recent count.

Hence if the first callback takes a long time, there's a slight possibility that the count could have changed by the time the second callback executes. However, React does its best to handle this scenario efficiently and give you the most up-to-date count. The batching mechanism ensures that multiple state updates are processed together, minimizing unnecessary renders.

Conclusion

So, there you have it – the magic behind React state updates and callbacks. Understanding this interesting process will not only make you look like a wizard in you React Interviews, but will also ensure your applications run smoothly and efficiently. Happy coding!