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.
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:
setStateusually appears asynchronous inside React synthetic events and lifecycle methods, but insetTimeout,Promise, or native DOM events, it often behaves more like a synchronous update - In React 18 and later: in most cases,
setStateis 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:
- Is the
setStatecall itself executed synchronously? - Can
this.stateor component state be read immediately after the call? - Does React start render and commit immediately?
- 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
setStategoes into the queue console.log(this.state.count)still sees the old value- The second
setStateis 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
setStateitself 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
setStateruns, 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,
setStateis asynchronous - Inside
setTimeout, native events, orPromisecallbacks, 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:
setTimeoutPromise.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
setStatebecome an async function? - Can I
await setState()now? - Does
setStateitself 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:
| Version | setState inside React events | setState inside setTimeout / Promise / native events | What drives the behavior |
|---|---|---|---|
| Before React 18 | Usually batched | Often behaves like an immediate update | Depends on batching context |
| React 18 and later | Automatically batched | Automatically batched | Depends 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,
setStateinside React synthetic events and lifecycle methods usually entered React's batching flow, so it behaved more like an asynchronous update. But insidesetTimeout, 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 withcreateRoot, 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 useflushSyncto 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
setStateappears 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."
Related reading
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