RoadmapDay 23 / 80
React (Web)Month 2 · Week 5

Day 23: State Batching & Concurrent Features (useTransition, useDeferredValue)

Use React 18's automatic batching and Concurrent APIs to keep typing/interaction responsive while expensive re-renders happen in the background.

Mark this day complete

Study

Concepts

Automatic batching (React 18+)

Batching means React groups multiple `setState` calls that happen within the same event/task into a SINGLE re-render, instead of re-rendering after each call. Before React 18, batching only happened inside React event handlers; calls inside a `setTimeout`, a Promise `.then`, or a native event listener each triggered their own separate render. React 18's automatic batching extends this grouping to ALL of those cases by default, which is a meaningful perf win with zero code changes — and occasionally a surprise if code relied on the old, more eager re-render timing.

`flushSync(fn)` is the escape hatch that forces a synchronous, un-batched render for cases where you genuinely need the DOM updated immediately after a state change (rare — e.g. measuring layout right after a change).

Marking updates as low priority with useTransition

Fiber's interruptible render phase (Day 21) enables priority: some updates (typing in a text box) must feel instant, while others (re-filtering a 10,000-row list based on that text) can wait a few milliseconds without the user noticing. `useTransition` lets you wrap a state update in `startTransition(() => setState(...))`, telling React "this update is not urgent — keep the UI responsive to more urgent updates (like further typing) even if it means delaying or discarding in-progress work on this one." `isPending` reports whether that low-priority work is still in flight, so you can show a subtle loading indicator instead of freezing.

`useDeferredValue(value)` is the sibling API for when you cannot wrap the SETTER in a transition (e.g. the value comes from a prop, not local state) — it returns a version of `value` that lags behind during urgent updates and catches up once the main thread is free, letting you render the expensive part off a "stale-but-cheap" value momentarily.

See It

Visualizations

Visualization

useTransition vs useDeferredValue

 useTransitionuseDeferredValue
You controlThe setState call itselfA value you already receive
Typical sourceLocal state you ownProps or context you don't own
Gives youisPending booleanA lagging copy of the value
Use whenTriggering a low-priority update yourselfConsuming a value that changes urgently elsewhere

Visualization

Typing while a transition is pending

t=0ms

User types "a"

urgent update: input renders instantly

t=0ms

startTransition schedules the filtered list

marked low priority

t=8ms

User types "b"

urgent — interrupts the in-progress low-priority render

t=40ms

Filtered list finally commits

for the latest value only — intermediate work discarded

Build It

Code Examples

Automatic batching in React 18

jsx
function handleClick() {
  fetch('/api/data').then(() => {
    setCount((c) => c + 1);   // React 18: batched...
    setFlag((f) => !f);       // ...with this — ONE re-render total,
                                // even though we're inside a Promise callback.
  });
}

Keeping a search input responsive with useTransition

jsx
function SearchableList({ allItems }) {
  const [query, setQuery] = useState('');
  const [filtered, setFiltered] = useState(allItems);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // urgent: the input must update instantly

    startTransition(() => {
      // low priority: can be interrupted by the next keystroke
      setFiltered(allItems.filter((i) => i.name.includes(value)));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <span>Updating…</span>}
      <ExpensiveList items={filtered} />
    </>
  );
}

Remember

Key Takeaways

  • React 18 batches ALL setState calls within one task by default — timers, promises, and native listeners included.
  • flushSync is the rare escape hatch to force an immediate, un-batched render.
  • useTransition wraps a setState call you own, marking it low-priority and giving you an isPending flag.
  • useDeferredValue lags a value you don't control the setter for, letting urgent renders skip ahead of it.
  • Both APIs rely entirely on Fiber's interruptible render phase — they are impossible without the Day 21 architecture.

Do It

Practice

  1. 1Build a slow-rendering list (1,000+ items with an artificial render delay) filtered by a text input; feel the lag, then fix it with useTransition.
  2. 2Reproduce the pre-React-18 vs React-18 batching difference by counting renders inside a setTimeout callback with two setState calls.
  3. 3Swap useTransition for useDeferredValue in the search example by lifting query into a parent and passing it down as a prop.