Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
💾 Introduction to State⚛️ Using useState Hook🔄 Updating State Correctly🎯 Initial State Values🚫 Common State Mistakes📊 Multiple State Variables🔗 State & Rendering📝 Forms with State🏗️ State Structure Best Practices
Reactjs/State/Updating State

⚡ State Updates and Batching — Mastering React 18's Automatic Batching

React 18 introduced automatic batching for state updates. Understanding how updates are grouped, when renders happen, and the timing of state changes is crucial for predictable, performant applications.


🎯 What is Batching and Why It Matters

Batching groups multiple state updates into a single render. Without batching, each setState call triggers a render, causing performance issues and UI flickering.

In React 18, batching happens automatically for promises, setTimeout, and event handlers. This means multiple setters in a row update once, not separately.

import { useState } from 'react';

function BatchingDemo() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const [renderCount, setRenderCount] = useState(0);

  // ❌ WITHOUT UNDERSTANDING BATCHING
  // This looks like 3 renders, but React 18 batches it to 1
  const handleMultipleUpdates = () => {
    setCount(count + 1);
    setName('Updated');
    setRenderCount(renderCount + 1);
    // React batches these into ONE render cycle
  };

  // Multiple updates in async still batch in React 18
  const handleAsyncUpdates = async () => {
    const data = await fetch('/api/data').then(r => r.json());
    setCount(data.count);      // Batched!
    setName(data.name);        // Batched!
    // Both updates trigger ONE render
  };

  // Updates in promises are batched
  const handlePromiseUpdates = () => {
    Promise.resolve()
      .then(() => setCount(count + 1))    // Batched
      .then(() => setName('Changed'));    // Batched together
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Name: {name}</p>
      <p>Renders: {renderCount}</p>
      <button onClick={handleMultipleUpdates}>Multiple Updates</button>
      <button onClick={handleAsyncUpdates}>Async Updates</button>
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: Batching across different update sources

function AdvancedBatchingDemo() {
  const [count, setCount] = useState(0);
  const [status, setStatus] = useState('idle');
  const [error, setError] = useState<string | null>(null);

  // Event handler: ALL batched to 1 render
  const handleEventBatching = () => {
    setCount((c) => c + 1);
    setStatus('updating');
    setError(null);
  };

  // Promise chain: ALL batched to 1 render
  const handlePromiseBatching = () => {
    Promise.resolve()
      .then(() => {
        setCount((c) => c + 1);
        setStatus('processing');
      })
      .then(() => setError(null));
  };

  // Async function: ALL batched to 1 render
  const handleAsyncBatching = async () => {
    setStatus('loading');
    setError(null);

    try {
      const result = await fetch('/api/data');
      const data = await result.json();
      setCount(data.count);
      setStatus('loaded');
    } catch (err) {
      setError((err as Error).message);
      setStatus('error');
    }
  };

  return (
    <div>
      <p>Count: {count}, Status: {status}</p>
      {error && <p>Error: {error}</p>}
      <button onClick={handleEventBatching}>Event Batch</button>
      <button onClick={handlePromiseBatching}>Promise Batch</button>
      <button onClick={handleAsyncBatching}>Async Batch</button>
    </div>
  );
}

</details>

💡 Timing and State Visibility

State updates are not immediately visible. They queue up and apply during the next render. Understanding when state changes are visible prevents bugs with stale values.

When you call a setState function, it doesn't immediately change the value. Instead, React queues the update for the next render cycle. Within that same event handler, the state value stays the same.

import { useState, useCallback } from 'react';

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

  // State doesn't change until next render
  const handleClick = () => {
    console.log('Before:', count);  // 0
    setCount(count + 1);
    console.log('After setter:', count);  // Still 0!

    // Count won't be 1 until this handler finishes
    // and React renders the component again
  };

  // To use updated state immediately, use callback
  const handleClickWithCallback = () => {
    const newCount = count + 1;
    setCount(newCount);
    // Use newCount immediately, not state
    console.log('New value:', newCount);
  };

  // Functional updates give you previous value
  const handleClickFunctional = () => {
    setCount((prev) => {
      const newCount = prev + 1;
      console.log('In updater:', newCount);
      return newCount;
    });
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Click (see console)</button>
      <button onClick={handleClickWithCallback}>Click with local var</button>
      <button onClick={handleClickFunctional}>Click with functional</button>
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: Complex timing with async operations

function FetchComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchData = async () => {
    // These updates batch together
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/data');
      const json = await response.json();

      // These updates also batch together (different batch from above)
      setData(json);
      setLoading(false);
    } catch (err) {
      // These batch together
      setError((err as Error).message);
      setLoading(false);
    }
  };

  // Show which batch happened
  const handleClick = () => {
    setLoading(true);
    setTimeout(() => {
      // Different batch - setLoading(false) above was already rendered
      setLoading(false);
    }, 0);
  };

  return (
    <div>
      {loading && <p>Loading...</p>}
      {data && <p>Data: {JSON.stringify(data)}</p>}
      {error && <p>Error: {error}</p>}
      <button onClick={fetchData}>Fetch</button>
      <button onClick={handleClick}>Delayed Update</button>
    </div>
  );
}

</details>

🔧 When NOT to Batch - flushSync

In rare cases, you need updates to render immediately without batching. React provides `flushSync` for this.

`flushSync` forces React to render synchronously, flushing all batched updates immediately. Use sparingly, only when necessary for DOM measurements or third-party library integration.

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

function FlushSyncDemo() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState<string[]>([]);

  // ❌ NORMAL: Updates batch, list might jump visually
  const addItemBad = () => {
    setCount(count + 1);
    setItems([...items, `Item ${count}`]);
    // Both render together
  };

  // ✅ GOOD: Force sequential renders for smooth UX
  const addItemGood = () => {
    flushSync(() => {
      setCount(count + 1);
    });
    // Count renders now

    flushSync(() => {
      setItems([...items, `Item ${count + 1}`]);
    });
    // List renders now
  };

  // Real use case: DOM measurement
  const handleMeasure = () => {
    setCount(count + 1);

    // Measure DOM after render completes
    flushSync(() => {
      // Force render now so DOM is updated
    });

    const height = document.getElementById('container')?.offsetHeight;
    console.log('Height:', height);
  };

  return (
    <div id="container">
      <p>Count: {count}</p>
      <ul>
        {items.map((item, i) => (
          <li key={i}>{item}</li>
        ))}
      </ul>
      <button onClick={addItemBad}>Add Normal</button>
      <button onClick={addItemGood}>Add with flushSync</button>
      <button onClick={handleMeasure}>Measure DOM</button>
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: flushSync with event handlers and integration

function ThirdPartyIntegration() {
  const [value, setValue] = useState('');

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newValue = e.target.value;

    // Update React state synchronously
    flushSync(() => {
      setValue(newValue);
    });

    // Now the DOM is updated, safe to integrate with third-party
    if (window.externalLibrary) {
      window.externalLibrary.update(newValue);
    }
  };

  return (
    <div>
      <input value={value} onChange={handleInputChange} />
      <p>Value: {value}</p>
    </div>
  );
}

// Example: Sequential renders for animations
function AnimatedCounter() {
  const [count, setCount] = useState(0);
  const [displayCount, setDisplayCount] = useState(0);

  const increment = () => {
    const newCount = count + 1;

    // Update count first
    flushSync(() => {
      setCount(newCount);
    });

    // Animate to display it
    flushSync(() => {
      setDisplayCount(newCount);
    });
  };

  return (
    <div>
      <p style={{ fontSize: `${displayCount * 20}px` }}>Count: {displayCount}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

</details>

🔧 Concurrent Rendering and Transitions

React 18 introduced `useTransition` and `useDeferredValue` for fine-grained control over update priority.

Transitions let you mark updates as non-urgent. They can be interrupted by urgent updates (like user input), enabling responsive UIs even with heavy computations.

import { useState, useTransition } from 'react';

function TransitionDemo() {
  const [searchTerm, setSearchTerm] = useState('');
  const [filteredList, setFilteredList] = useState<string[]>([]);
  const [isPending, startTransition] = useTransition();

  // Generate large list for demo
  const allItems = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;

    // Urgent update: input responds immediately
    setSearchTerm(value);

    // Non-urgent update: filter happens in background
    startTransition(() => {
      const filtered = allItems.filter((item) =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredList(filtered);
    });
  };

  return (
    <div>
      <input
        value={searchTerm}
        onChange={handleSearch}
        placeholder="Search..."
      />
      {isPending && <p>Filtering...</p>}
      <p>Results: {filteredList.length}</p>
      <ul>
        {filteredList.slice(0, 10).map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

<details>

<summary>📚 More Examples</summary>

// Example: useTransition with async operations

function AsyncTransitionDemo() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = async (searchQuery: string) => {
    setQuery(searchQuery);

    // Mark async work as non-urgent
    startTransition(async () => {
      const response = await fetch(`/api/search?q=${searchQuery}`);
      const data = await response.json();
      setResults(data);
    });
  };

  return (
    <div>
      <input
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search"
      />
      {isPending && <p>Loading results...</p>}
      <ul>
        {results.map((result) => (
          <li key={result}>{result}</li>
        ))}
      </ul>
    </div>
  );
}

// Example: useDeferredValue for expensive computations
import { useDeferredValue, useMemo } from 'react';

function DeferredValueDemo() {
  const [input, setInput] = useState('');
  const deferredInput = useDeferredValue(input);

  // This expensive computation uses deferred value
  const results = useMemo(() => {
    let count = 0;
    for (let i = 0; i < 100000; i++) {
      if (String(i).includes(deferredInput)) count++;
    }
    return count;
  }, [deferredInput]);

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Type..."
      />
      <p>Input: {input}</p>
      <p>Matches: {results}</p>
    </div>
  );
}

</details>

📊 Update Timing and Batching Comparison

┌─────────────────────┬──────────────────┬─────────────────┐
│ Context             │ Batching Status  │ Behavior        │
├─────────────────────┼──────────────────┼─────────────────┤
│ Event handlers      │ Auto batched     │ Single render   │
│ Promises/async      │ Auto batched     │ Single render   │
│ setTimeout          │ Auto batched     │ Single render   │
│ flushSync           │ Not batched      │ Immediate       │
│ Transitions         │ Low priority     │ Can be deferred  │
│ useDeferredValue    │ Deferred         │ Delayed render  │
└─────────────────────┴──────────────────┴─────────────────┘

🔑 Key Takeaways

  • ✅ React 18 auto-batches all state updates by default
  • ✅ Multiple setState calls in same event handler = one render
  • ✅ State changes don't apply until next render finishes
  • ✅ Use functional updates to access previous state value
  • ✅ flushSync forces synchronous render (use rarely!)
  • ✅ useTransition marks updates as non-urgent
  • ✅ useDeferredValue defers expensive computations
  • ✅ Batching improves performance and prevents flickering
  • ✅ Understand timing to debug state issues

Ready to practice? Challenges | Next: Complex State Structures


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn