React.js course ยท Lecture 10 of 27

Updating State Correctly & Batching

A masterclass tutorial covering Direct State Updates vs Functional Updates (prev => prev + 1), Automatic Batching in React 18, Multiple State Updates, Immutability enforcement, Race Conditions, Common Pitfalls, and Interview Questions.

  • 30 min read
  • Intermediate
  • 8 topics
  • 20 Sept 2026
  • Ravindra Nath Jha

Why Direct State Updates Fail

In React, state updates are asynchronous and batched for performance.

If you call setCount(count + 1) multiple times synchronously within a single event handler, React uses the stale count value from the current render snapshot for all updates!

JavaScript
// โŒ FAILS TO INCREMENT BY 3:
const handleClick = () => {
  setCount(count + 1); // count is 0 -> scheduled 1
  setCount(count + 1); // count is STILL 0 -> scheduled 1
  setCount(count + 1); // count is STILL 0 -> scheduled 1
};
// Result: Count increases by 1, NOT 3!

The Solution โ€” Functional State Updates (prev => prev + 1)

When your new state depends on the previous state value, you MUST pass a updater callback function to the state setter.

Syntax:

setCount(prevCount => prevCount + 1);

React queues updater functions in order and passes the pending state to each callback:

JavaScript
// โœ… CORRECTLY INCREMENTS BY 3:
const handleClick = () => {
  setCount(prev => prev + 1); // prev 0 -> returns 1
  setCount(prev => prev + 1); // prev 1 -> returns 2
  setCount(prev => prev + 1); // prev 2 -> returns 3
};
// Result: Count increases by 3!

Automatic Batching in React 18

What is Batching?

Batching is when React groups multiple state updates into a single re-render to prevent unnecessary DOM renders and improve app speed.

  • React 17 & Older: React batched state updates inside React event handlers only (like onClick). State updates inside setTimeout, promises, or native fetch calls triggered separate re-renders.
  • React 18 Automatic Batching: React 18 automatically batches ALL state updates regardless of where they originate (Promises, setTimeout, Native Event Handlers, Async/Await)!

Overriding Automatic Batching (flushSync)

In rare edge cases where you need immediate DOM mutation measurement right after a state update, React provides flushSync from react-dom:

JavaScript
import { flushSync } from 'react-dom';

flushSync(() => {
  setCount(count + 1); // Forces immediate DOM update
});

State Immutability Rule

State objects and arrays in React must be treated as Immutable.

Instead of modifying existing object properties directly, always create a new object reference using spread operators (...) or array transformation methods (map, filter, slice).

Why Object Reference Equality Matters:

React checks if a component should re-render using shallow reference comparison (Object.is(oldState, newState)). If you mutate the old object in-place, the memory reference remains identical, and React cancels the re-render!

Common Mistakes when Updating State

  1. Using stale state values in async callbacks: Always use functional updates (prev => ...) inside timers or fetch responses.
  2. Mutating state variables directly before calling setter: state.count++; setCount(state.count);.
  3. Assuming setter function executes synchronously: Calling setCount(5); console.log(count); prints the old value because re-render hasn't happened yet.

Top React Interview Questions on State Updates

Q1: What is Automatic Batching in React 18?

*Answer*: Automatic Batching is a performance feature where React 18 combines multiple state updates across timeouts, promises, and event handlers into a single re-render pass.

Q2: When should you use functional state updates (setCount(prev => prev + 1))?

*Answer*: Whenever the new state relies on the previous state value or when triggering multiple sequential updates within the same handler block.

Q3: Why doesn't React update state synchronously?

*Answer*: Synchronous updates would cause cascading re-renders across parent and child components for every single state line, drastically degrading UI performance.

Practical Hands-On Exercise โ€” Sequential State Updater Test Bench

Here is an interactive test bench comparing direct state updates vs functional state updates side-by-side:

JavaScript
// Complete Masterclass State Updating Showcase (src/App.jsx)
import React, { useState } from 'react';

export default function App() {
  const [directCount, setDirectCount] = useState(0);
  const [functionalCount, setFunctionalCount] = useState(0);

  // Stale State Bug Demo
  const handleTripleDirect = () => {
    setDirectCount(directCount + 1);
    setDirectCount(directCount + 1);
    setDirectCount(directCount + 1);
  };

  // Functional Update Fix Demo
  const handleTripleFunctional = () => {
    setFunctionalCount(prev => prev + 1);
    setFunctionalCount(prev => prev + 1);
    setFunctionalCount(prev => prev + 1);
  };

  return (
    <div style={{ maxWidth: "600px", margin: "30px auto", padding: "20px", background: "#f8fafc", borderRadius: "16px" }}>
      <h2 style={{ color: "#002057" }}>Lecture 10: State Updating & Batching Explorer</h2>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px", marginTop: "20px" }}>
        {/* Direct Update Box */}
        <div style={{ background: "#fef2f2", padding: "16px", borderRadius: "12px", border: "1px solid #fca5a5" }}>
          <h4 style={{ color: "#991b1b" }}>โŒ Direct Updates</h4>
          <p>Count: <strong>{directCount}</strong></p>
          <button onClick={handleTripleDirect} style={{ background: "#ef4444", color: "#fff", border: "none", padding: "8px 12px", borderRadius: "6px" }}>
            Add +3 (Direct)
          </button>
          <small style={{ display: "block", marginTop: "8px", color: "#7f1d1d" }}>Increments by only +1 due to stale closure state!</small>
        </div>

        {/* Functional Update Box */}
        <div style={{ background: "#f0fdf4", padding: "16px", borderRadius: "12px", border: "1px solid #86efac" }}>
          <h4 style={{ color: "#166534" }}>โœ… Functional Updates</h4>
          <p>Count: <strong>{functionalCount}</strong></p>
          <button onClick={handleTripleFunctional} style={{ background: "#16a34a", color: "#fff", border: "none", padding: "8px 12px", borderRadius: "6px" }}>
            Add +3 (Functional)
          </button>
          <small style={{ display: "block", marginTop: "8px", color: "#14532d" }}>Correctly increments by +3 using (prev => prev + 1)!</small>
        </div>
      </div>
    </div>
  );
}