Skip to content
·4 min read

React 19 State Batching Changes How You Write Forms

Three setStates in a row have always been one render, but React 19 goes further: automatic batching in promises and native handlers, plus form actions that reset inputs without a single state variable.

ReactReact 19FormsJavaScriptWeb Dev

Write this handler and guess how many renders happen:

function handleSignup() {
  setEmail(emailInput);
  setPassword(passwordInput);
  setStep("confirm");
}

One. React collects all three updates and commits a single render. Batching has been default behavior for years, yet I still review code written to defend against a problem that stopped existing, and missing the one place it still does.

Step 1: Understand what batching already does

Inside an event handler, React does not render per setState. It runs your function to completion, queues every update, and renders once with the combined result. Three state changes, one render, one commit, one paint.

Two consequences people get backwards. First, reading a state variable right after setting it gives you the old value, because the render has not happened yet. Second, performance "optimizations" that merge three useStates into one object to "avoid re-renders" save nothing. They just make the code harder to write.

Step 2: Count on batching in async code too

React 17 batched handler updates only. A setTimeout or promise callback leaked one render per setState. React 18 made automatic batching universal:

setTimeout(() => {
  setA(1);
  setB(2);
  setC(3);
}, 1000);
// React 17: three renders. React 18+: one.

The same covers await continuations, native event listeners, and fetch callbacks. unstable_batchedUpdates, the escape hatch some libraries shipped, is gone from the conversation because the default finally matches what everyone assumed it was.

Step 3: Stop syncing inputs with mirror state

The old form pattern: one useState per field, an onChange per input, and a reset function that clears eight states on submit. React 19 form actions make most of that scaffolding optional:

async function createOrder(formData: FormData) {
  await submitOrder(formData);
  // React resets the <form> automatically on successful action
}
 
<form action={createOrder}>
  <input name="address" />
  <button>Order</button>
</form>

The uncontrolled input carries its own state, the action receives everything through FormData, and the reset is native. Fewer state variables means fewer batching puzzles to reason about, which is the cheapest optimization in this whole article.

Step 4: Batch around awaits deliberately

Here is the case that still produces two renders:

async function handleSave() {
  setSaving(true);
  await api.save(data);
  setSaving(false);
}

The two setSaving calls run in different ticks, so the component renders once to show the spinner and once to clear it. That is usually exactly what you want, and naming it is the point: batching groups updates within a tick, not across an await. If you ever need the before-and-after in a single commit, restructure so both updates land in the same continuation.

Step 5: Read state with the updater form when order matters

Batched updates queue in order, which trips people who read the variable mid-handler:

setCount(count + 1); // uses the stale count
setCount(count + 1); // still the same stale count
// result: count + 1, not count + 2
 
setCount((c) => c + 1);
setCount((c) => c + 1);
// result: count + 2, each updater sees the previous result

The updater form threads each queued result into the next call. Any time two updates touch the same value in one handler, the function form is not a style preference, it is the difference between correct and off-by-one.

The habit that ties it together

Batching means you write handlers the obvious way: set what needs setting, in the order the logic reads, and let React commit once. The remaining bugs come from the two edges, stale reads mid-handler and renders split by an await, and both have one-line fixes.

Migrating a form-heavy React app to 19? The action-based patterns above are a big chunk of my recent client work. Tell me about the app and I will show you which forms collapse into actions first.