Is React’s setState Asynchronous?

For this post I am talking about in React when you use (see: https://react.dev/reference/react/useState):

const [foo, setFoo] = useState(“bar”);

And then you use:

setFoo(“fizz”)  ← This right here

Let me ask you a question:

Is setState Asynchronous?

  1. Yes!
  2. No!

Trick question, the answer is “Well, it’s really complicated, and you should just assume it is.”

However…If you want more information about it, I can give you an answer in several parts…It’s complicated.

I feel like the true answer depends on the exact definition of “asynchronous.” So let’s explore how `setState` actually works.

Going deeper:

setState executes in two parts, the first part executing immediately, the second part executing later, however that second part is immediately added to the event loop…Actually that second part isn’t totally true.  It’s not technically added to the event loop – it tells React that it needs to re-render, the check for that is already on the event loop in React’s internal event loop…So technically, it’s not added to the end of the event loop, basically a flag is set saying “do a re-render” and that re-render happens after the current render finishes, when React gets to that check in its internal event loop.

We need to be clear about two different parts of state in React for this to make sense.  

  1. React maintains an internal state that is (usually – it is exposed in updater functions…for brevity, I’m stoping here) not exposed to you, the developer.  
  2. There is also a component state in the example above, this is  foo.

These two states are sometimes the same, and they are sometimes different.

The first thing it does is execute an update to React’s internal state (#1 above).  That is totally immediate, and I don’t think anyone would argue that it is asynchronous.  

When React’s internal state is updated, React tells itself  “I need to do a re-render.”  This re-render however is deferred till after the current render cycle is completed.

This means that the state inside your component (in the example at the top foo, #2 above) is NOT updated till that re-render.  The state inside your component only ever changes during render.  This is true for anything involving state in your component.  More simply: component state only ever changes during the render cycle.

So, is that second part asynchronous?

Well, you can’t await is, so no, it’s not, end of story…Except you can’t await setTimeout and I think we generally agree that setTimeout is asynchronous…You can however wrap setTimeout in a Promise and you can await that…Turns out, you can also wrap a setState in a promise and await that…But don’t ever do that because it makes React unhappy and throw errors.

Fact: React will always execute setStates in the same order, so the last one will always be the final value.  

Fact: You need to use an updater function if you want to access the current internal React value (#1 above) – meaning, if the current render has updated the state, the only way to see that updated state is in an updater function. 

Fact: You CANNOT access the current internal React value of a DIFFERENT state value (during the current render cycle) in your component, even in an updater function.  Meaning, if you have two state values, and you update one, then you update the second one – with or without an updater function – you will ALWAYS get the un-updated value of the first one.  Why: Because component state (#2 above) only changes on the re-render, and that doesn’t happen till after the current render completes.

By “asynchronous” do we mean “doesn’t execute immediately?”  Do we mean “Is added to the microtask queue?” …Does setTimeout(..., 0) count as asynchronous? A lot of what I read says “does not hold up execution” which well, it doesn’t, except it does after other stuff…

Well, that lead me to reading the ECMAScript spec about setTimeout and I couldn’t discern if setTimeout(..., 0) is added to the event loop, added to the microtask queue, one of the previous but with a slight delay, or something else…I’m actually not sure that the behavior is defined – If someone smarter than me knows the answer to this please let me know.

What I do know is that a setTimeout(...,0) will always execute after the re-render cycle (I know this because it obviously isn’t part of React’s render cycle and always is the final update – in my testing) – meaning, that if you have a setTimeout(...,0) that sets the state, as well as other settings of the same state, the final value will always be the one set inside of the setTimeout(...,0) …Except that I say “always” and I actually don’t actually know if that is true.  It is true in my testing.  If that setTimeout is added to the microtask queue, in between other tasks that set that state, then it is possible that it won’t be the final value…but I don’t know if it is…but generally it is true – at least in my testing…And again, I’m not totally positive that is even defined in the spec…and we are splitting hairs here.

Because I don’t think that is complicated enough, React was kind enough to make running in dev mode as opposed to prod work differently.  Well, kind of.  If you are using an updater function, React will update twice in dev mode, and once in prod.  Why?  Oh god how deep does this hole go? 

Short answer: it should be a pure function. (see: https://en.wikipedia.org/wiki/Pure_function & https://react.dev/learn/keeping-components-pure)

Technically when React tells itself it needs to re-render, it applies the update to that component state var (#2) to it’s queue with the current internal value (#1) of the variable – meaning that changes to that variable inside of an updater function ARE NOT SEEN – as the original value was already applied, when the call was queued.  So if you update the state of the variable inside of an updater function, and then try to update it again later with an updater function, the first update is ignored.  Meaning: that’s a really bad idea.  So, in dev mode React will run it twice, once with the first value, once with the second value, and if they are different, ya done goofed.  The reason it does this in dev mode is to show you that you goofed.

So again, how is “asynchronous” technically defined?  And is it asynchronous?  IDFK.

I say setState is not asynchronous because the execution order is defined, and everything it does is immediately added to the the event loop when it is called – if you know what you are doing, the results are deterministic, you absolutely know what the final result will be.  I say please don’t ever rely on this, because the next person who has to modify the code – including future you – is generally not smart enough to understand the nuances here, and if your code relies on this behavior, they will likely break things.

I also say it is asynchronous because part of it executes out of order, and we can (in theory) use a promise to `await` it.

Additionally – because this behavior is so esoteric, I don’t know that it will not be changed in React, sometime the future.

ALL OF THIS IS WHY I SAY TO JUST TREAT IT AS ASYNCHRONOUS! 

I probably made some technical mistakes above…Though I do think it is basically correct.  What I wrote is based on my reading many things, watching many things, and a butt load of tests I wrote myself….Really, I should have saved those tests so I could post them…If you want me to reproduce and post those test, let me know.

PLEASE LET ME KNOW IF ANYTHING I WROTE ABOVE IS INACCURATE.