Skip to content
DevDepth
← Back to all articles

In-Depth Article

Is setState Synchronous or Asynchronous in React? React 18 vs Earlier Versions

Learn when setState appears synchronous or asynchronous in React, why the answer changes across calling contexts, and how React 18 automatic batching changes the behavior.

Published: Updated: 11 min readreact-internals

This is a very common interview question, but if you just answer "asynchronous," you're likely to miss the point.

Whether setState behaves more like a synchronous or asynchronous update depends on three things: the React version, the calling context, and the execution context React is in at that moment.

A more accurate summary would be:

  • Before React 18: setState usually appears asynchronous inside React synthetic events and lifecycle methods, but in setTimeout, Promise, or native DOM events, it often behaves more like a synchronous update
  • In React 18 and later: in most cases, setState is automatically batched, so overall it behaves more like an asynchronous update

One thing to make clear up front: when we say "synchronous" or "asynchronous" here, we do not mean that setState becomes a Promise, or that it behaves like await. What we're really discussing is this:

After calling setState, does React commit the update immediately or not?

That's what this article is about.

If you want companion context, this pairs well with how React updates move from setState to a DOM commit, how concurrent rendering works under the hood, and why useEffect and useLayoutEffect run at different points in commit.

First, let's be precise: why "sync or async" is the wrong one-line answer

A lot of people memorize the line: "setState is asynchronous."

The reason that answer causes confusion is that it mixes together several different questions:

  1. Is the setState call itself executed synchronously?
  2. Can this.state or component state be read immediately after the call?
  3. Does React start render and commit immediately?
  4. Will multiple updates be merged?

What usually confuses people is really the second and third points.

In other words, what most people are actually asking is not "Is this function asynchronous?" but:

Why do I sometimes read the old value right after calling setState, and other times I get the new one?

To answer that properly, we have to look at how React's update mechanism works across versions.

1. Before React 18: the key is not "async," but the batching context

Before React 18, especially in legacy mode, setState was not really driven by a modern async scheduling model.

It worked more like this:

  • React would collect updates in certain controlled contexts
  • Wait until the current piece of logic finished running
  • And then process those updates together

That is why it often felt asynchronous.

So the async feeling in older React mostly came from batching, not from actually pushing work to the next event loop turn.

1.1 In older versions, React first checks whether it is in "batching mode"

You can think of React as having an internal switch, often explained as isBatchingUpdates.

You do not need to memorize the variable name. What matters is the idea:

  • If React is currently inside its own controlled execution flow, it will not update immediately. It will queue the update first
  • If React is no longer inside that context, it may process the update right away

So in older versions, whether setState looked synchronous or asynchronous depended less on the syntax you wrote, and more on this:

Did this call happen inside React's own managed context, or outside of it?

1.2 Why does setState look asynchronous inside React events?

Let's start with the most common case: a React event handler.

class Demo extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    this.setState({ count: 1 });
    console.log(this.state.count); // In pre-React 18 behavior, this is usually still 0

    this.setState({ count: 2 });
  };

  render() {
    return (
      <button onClick={this.handleClick}>
        count: {this.state.count}
      </button>
    );
  }
}

In this case, React enters its own event handling flow first. At that point, it knows:

  • This code is running inside React's event system
  • There may be multiple updates during this handler
  • So it is more efficient not to re-render after every single one

So what happens is:

  • The first setState goes into the queue
  • console.log(this.state.count) still sees the old value
  • The second setState is also queued
  • Once the event handler finishes, React flushes the updates together

So the async feeling here comes from this:

The update is temporarily deferred, not because setState itself became an async API.

1.3 Why does it often look synchronous inside setTimeout?

Now look at a classic older React example:

class Demo extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    setTimeout(() => {
      this.setState({ count: 1 });
      console.log(this.state.count); // Before React 18, this often prints 1
    }, 0);
  };

  render() {
    return (
      <button onClick={this.handleClick}>
        count: {this.state.count}
      </button>
    );
  }
}

The key here is that when the setTimeout callback runs, it is no longer inside the original React event transaction.

That means:

  • The React event handler has already finished
  • The batching context React set up is gone
  • Now when setState runs, React often processes it immediately

So it ends up behaving more like a synchronous update:

  • You call setState
  • React updates right away
  • Then you read this.state, and it may already be the new value

That is why older React explanations often said:

  • Inside React events, setState is asynchronous
  • Inside setTimeout, native events, or Promise callbacks, it is synchronous

More precisely, it is not that one is truly async and the other truly sync. It is this:

The first runs inside React's batching context, while the second often runs outside of it.

1.4 What older React was really doing: delaying, not truly scheduling

This is also why people sometimes describe pre-React 18 behavior as pseudo-asynchronous.

React was not necessarily scheduling render into a clearly separate future async phase. In many cases it was simply saying:

  • Do not process this update yet
  • Finish the current controlled flow first
  • Then flush the updates together

You can think of it like this:

Not "run this in a future async task," but "hold on, let me finish what I'm doing first."

That distinction matters, because it explains why behavior could differ so much by context in older versions.

2. React 18 and later: the focus shifts from batching lock to unified scheduling

In React 18, the update model changed in an important way.

React no longer relies mainly on whether updates happen inside a specific batching context. Instead, it uses a more unified scheduling model. Two keywords show up a lot here:

  • Scheduler
  • Lanes

You do not need to go deep into them right away. The main change is this:

In React 18, updates usually enter the scheduling pipeline first instead of triggering an immediate render.

And that leads directly to one of the biggest changes in React 18:

Automatic batching.

2.1 What does automatic batching mean in React 18?

It is actually pretty simple.

Before React 18, batching mainly happened in logic React itself wrapped and controlled. In React 18, as long as conditions allow, React will try to batch updates even if they happen inside:

  • setTimeout
  • Promise.then
  • Native DOM events
  • Other async callbacks

So after React 18, the behavior of setState feels much more consistent. You no longer see as much of that sharp split where:

  • Updates in event handlers feel async
  • Updates in timeout callbacks feel sync

2.2 The most typical difference: updates inside setTimeout are batched too

Here is a React 18-style example:

import { useState } from "react";

export default function Demo() {
  const [name, setName] = useState("Tom");
  const [age, setAge] = useState(20);

  console.log("render");

  const handleClick = () => {
    setTimeout(() => {
      setName("Alice");
      setAge(25);
    }, 0);
  };

  return (
    <div>
      <p>{name} - {age}</p>
      <button onClick={handleClick}>update</button>
    </div>
  );
}

In a React 18 createRoot environment, those two updates are usually batched into a single render.

That is very different from older React, where this kind of code more often caused multiple renders.

The reason is that React 18 handles updates differently:

  • Call setState
  • Create an update
  • Hand it over to the scheduler
  • Process it at an appropriate time

The key question is no longer "Am I inside a React event right now?" It becomes:

Let me put this update into the scheduling system first, and then see how it can be processed together with other updates in the same batch.

2.3 Why does React 18 feel more asynchronous?

Because it is genuinely closer to real scheduling.

In older React, the async feeling often came from "finish this function, then flush right away." In React 18, it is more like this:

  • Updates enter the scheduling system first
  • Render does not necessarily happen immediately
  • It may run in a microtask or in a later slice of work

So if you want a rough shortcut:

  • Before React 18: more like "delay it briefly, then handle it synchronously"
  • React 18 and later: more like "schedule first, then execute"

That is why React 18 updates are often described as being closer to real async behavior.

3. The easiest point to misunderstand: setState itself did not become a Promise

This is worth calling out directly.

Once people hear that React 18 is more asynchronous, they often assume things like:

  • Did setState become an async function?
  • Can I await setState() now?
  • Does setState itself go into the microtask queue?

None of those are correct.

setState or setXxx is still just a normal function call. It does not return a Promise, and it does not have await semantics.

What changed is this:

How React handles the update, and when React decides to commit it.

So the accurate statement is not "setState became asynchronous." It is:

React's strategy for processing state updates changed across versions.

4. Why should you mention both version and context in an interview?

Because if you only say "setState is asynchronous," you are describing the surface behavior, not the reason behind it.

A good answer should include at least three layers:

Layer 1: distinguish pre-React 18 from React 18+

This is the biggest dividing line.

Layer 2: distinguish the calling context

For example: React synthetic events, lifecycle methods, setTimeout, Promise callbacks, native events.

Layer 3: explain the mechanism behind it

Not just the observed behavior, but why it happens:

  • Why older React depended on batching context
  • Why React 18 moved to a more unified scheduling model
  • Why automatic batching makes the behavior more consistent

If you answer it that way, it sounds like understanding, not memorization.

5. Special case: what if I really do want the update immediately?

There are exceptions, of course.

Sometimes you really do not want React to wait around and schedule things. For example:

  • You need the latest DOM size immediately
  • You want to start an animation right after an update
  • You need a specific update to commit right now

For that, React gives you an escape hatch:

flushSync

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

export default function Demo() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    flushSync(() => {
      setCount(1);
    });

    const text = document.getElementById("count")?.textContent;
    console.log(text); // At this point, the latest DOM is usually already available
  };

  return (
    <div>
      <span id="count">{count}</span>
      <button onClick={handleClick}>update</button>
    </div>
  );
}

The basic idea of this API is:

Do not queue this. Do not wait for later batching. Finish it now.

So when you use flushSync, React will try to complete that render and commit immediately, making it much more likely that subsequent DOM reads see the updated result.

That said, this is not something to overuse. React normally prefers batching and scheduling for good reasons: performance and consistency. If you force everything to be synchronous, you are opting out of those benefits.

So flushSync is best treated as a special-purpose escape hatch, not the default way to update state.

6. Putting the whole logic chain together

At this point, the full story becomes pretty straightforward.

1. setState is not a command that immediately changes state and then immediately changes the DOM

It is better understood as telling React: there is an update here, please process it.

2. Before React 18, whether that update was processed immediately depended heavily on whether React was still inside its batching context

That is why the exact same setState call could behave very differently inside a React event handler versus inside setTimeout.

3. In React 18 and later, updates usually enter the scheduling pipeline first

So whether the code runs in an event handler, a Promise callback, or setTimeout, it is much more likely to behave like an asynchronously batched update.

4. The word asynchronous here usually does not mean setState itself is an async function

It means React does not necessarily commit the update at the exact moment you call it.

5. If you truly need an immediate commit

You can use flushSync, but that is the exception, not the standard pattern.

7. The easiest version comparison to remember

If you just want the simplest summary table, this is a good one:

VersionsetState inside React eventssetState inside setTimeout / Promise / native eventsWhat drives the behavior
Before React 18Usually batchedOften behaves like an immediate updateDepends on batching context
React 18 and laterAutomatically batchedAutomatically batchedDepends on unified scheduling and priority lanes

But that table only helps you remember the surface behavior. The real understanding comes from the logic behind it.

8. A stronger interview answer template

If an interviewer asks, "Is setState synchronous or asynchronous?"

A solid answer would sound like this:

You cannot answer that with a single word. It depends on the React version and the calling context. Before React 18, setState inside React synthetic events and lifecycle methods usually entered React's batching flow, so it behaved more like an asynchronous update. But inside setTimeout, Promise callbacks, or native DOM events, it often behaved more like a synchronous update, because those calls happened outside React's batching context. In React 18, especially with createRoot, React introduced automatic batching, so in most cases updates enter the scheduling pipeline first and overall behave more like asynchronous updates. If you ever need the DOM to update immediately, you can use flushSync to force a synchronous commit.

The nice thing about this structure is:

  • You start with the conclusion
  • Then split by version
  • Then split by context
  • And finally explain the mechanism and the exception

That not only gets the answer right, it also shows that you understand the reasoning behind it.

9. One final sentence to sum it up

The real answer to whether setState is synchronous or asynchronous is not a binary choice. It is this:

Whether setState appears asynchronous depends on whether React, in that version and in that context, puts the update into its batching and scheduling pipeline.

Once that clicks, your understanding of React updates is already much better than simply memorizing "setState is async."

Reviewed by

DevDepth Editor

Editor and frontend engineering writer

DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.

Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.

Last editorial review: 2026-03-16

Contact the editor