It's not about JS, it's because React won't let you render an inconsistent view of state. `setState` allows it to queue up multiple state changes, commit them all, and only then re-render your component. So you should think of `setState` as "queuing up a change to be applied for the next render cycle".
Thanks for the clarification. I had thought that React updates state immediately and may preform multiple re-renders prior to committing to the DOM, and it's the re-renders which are queued.
React _usually_ queues up a render, which is then executed at the end of the event loop tick. This allows many calls to `setState()` in the same tick to result in a single render pass at the end.
The default behavior is that _if_ there are any changes that are needed based on the render, React then continues ahead and applies them to the DOM, and most of the time that is done immediately.
When a render is queued, React keeps the info about the requested state update as part of the queue, and those state changes are calculated and applied as part of the "render phase". So, if you happen to call `setState(2); setState(3);`, the second call completely overrides the first and there won't even be an attempt to render the component using the `2` value. (You can use the `setState(prevValue => 3)` form to force React to do each value separately.)
As of React 18, the new "concurrent rendering" features like `startTransition` and Suspense allow React to alter priorities of renders, and split the calculation into multiple chunks over time with pauses in between. When React _is_ done determining any changes in that render pass, it still applies them to the DOM in a single synchronous "commit phase".
The other caveat is that React will sometimes execute a full render+commit synchronously under specific conditions, such as a `setState` inside of a `useLayoutEffect` hook.
I wrote an extensive detailed post called "A (Mostly) Complete Guide to React Rendering Behavior" that tries to explain a lot of these nuances: