
[NOTE: Since I wrote this article, I've turned this code into an NPM package that can be found here:https://www.npmjs.com/package/@toolz/use-synchronous-state]
Since I've converted my dev to React Hooks (rather than class-based components), I keep running head-first into the asynchronous nature ofstate
updates. I don't honestly understand why this rarely seemed like a problem in my class-based components. But with functions/Hooks, I keep hitting this "roadblock". And unlike other articles I've written, this isn't my cocky declaration that I have solvedALL THE THINGS!!! Rather, I'd be thrilled if someone can show me an improvement on my proposed solution.
The Problem
We have a complex form. There are field-level validations. And form-level validations. And some of those validations vary based on the values entered into other fields.
Because the user's path through the form is not always linear, the logic in the component is broken up, as much as possible, into small manageable functions. So for example, when you update theMember ID
field, it callsupdateMemberId()
. Then it callsvalidateMemberId()
, to see if we should show any error messages to the user. Then it callsvalidateForm()
, to see if we should be checking all of the other fields on the form.
So the code ends up looking something like this:
exportdefaultfunctionApp(){const[memberId,setMemberId]=useState('');const[validateEntireForm,setValidateEntireForm]=useState(false);constupdateMemberId=userValue=>{setMemberId(userValue);validateMemberId();if(validateEntireForm)validateForm();}constvalidateForm=()=>{if(!validateEntireForm)setValidateEntireForm(true);validateMemberId();// validate the rest of the fields in the form}constvalidateMemberId=()=>{// validate based on the CURRENT value of 'memberId'returnvalidOrNot;}return(<>UXHere...</>);}
I won't ask you to mentally "load" this pseudo-code. So I'll just tell you the problem that I run into: Tabbing out of thememberId
field triggersupdateMemberId()
, which in turn updates thestate value ofmemberId
, which then leads to callingvalidateMemberId()
. InsidevalidateMemberId()
, we'll be referencing thestate value formemberId
- the value that was set microseconds previously insideupdateMemberId()
.
Of course, even though the value of thememberId
state variable was updated duringupdateMemberId()
, what happens whenvalidateMemberId()
tries to reference that same variable? That's right, it doesn't see thelatest value ofmemberId
. In fact, it sees whatever was saved intomemberId
during theprevious update. SovalidateMemberId()
is always one updatebehind.
Of course, this problem is only exacerbated if we've flipped thevalidateEntireForm
flag. Because oncevalidateForm()
gets called, it will also lead to referencing the value ofmemberId
- which will still be stuck on theprevious value.
The "problem" is pretty simple - and one that has been inherent in React since it was created. State updates areasynchronous. This was true in class-based components. It's true with functions/Hooks. But for whatever reason, I've only recently been running into ever-more headaches from this basic fact.
SincesetMemberId()
is asynchronous, subsequent references tomemberId
don't reflect the most up-to-date value that was just entered by the user. They reference theprevious value. And that obviously throws off the validation.
Standard (Poor) Solutions
There are several "standard" ways to address this problem. In some situations, they might be appropriate. But in most scenarios, I really don't care for them at all. They include:
Consolidate all these functions intoone routine. If it's all one function, then we can set one temp variable for the new field value, then use that same temp variable to update the field's state variable, and to check for field-level validity, and to check for global form validity. But if the "solution" is to stop creating small, targeted, single-use functions, well then... I don't really want to pursue that "solution" at all.
Explicitly pass the values into each function. For example,
updateMemberId()
could grab the newly-entered value and pass itintovalidateMemberId()
. But I don't like that. Why??? Well, because in this example, the state variable is thesystem of record. In other words, I don't wantvalidateMemberId()
to only validate whatever value was blindly passed into it. I want that function to validatethe current state value. And if that's to occur, the function should always be looking back intostate to grab the latest value. I've also found that, when building complex user interactions, there can sometimes be many different scenarios where a validation needs to be checked. And during those scenarios, there's not always a convenient variable to pass into the validation function. During those scenarios, it makes far more sense for the validation function to just grab the state value on its own.Use reducers. I dunno. Maybe it's because I hate Redux, but Ireally dislike feeling compelled to convert most of my calls to
useState()
intouseReducer()
. Once you go down theuseReducer()
path, more and moreand more of your logic ends up getting sucked out of your components and into all of these helper functions. And once it's sitting in all those helper functions, most devs feel compelled to start sorting them off into their own separate card catalog of directories. Before you know it, your previously-simple component has become an 8-file octopus of confusion.Use
useRef()
?? I've seen several references to this on the interwebs. Honestly, any time I start following this rabbit hole, I end up burning precious hours and getting no closer to a solution. IfuseRef()
is the answer to this problem, I'dlove to see it. But so far... it seems lacking.Use
useEffect()
Stop. No, seriously. Just...stahp. I've seen several threads on the interwebs suggesting that the "solution" to this quandary is to leverageuseEffect()
. The idea is that, for example, when we want to updatememberId
, we also create a call touseEffect()
that handles all of the side effects that happen once we updatememberId
. But that often threatens to turn the logic of our components on its ear. It's not uncommon for me to have a component where changingone state value forces me to check on the values ofseveral other state values. And once you start chunking all of that crap into the dependency array... well, you might as well just start building a whole new tree of Higher Order Components.Use theverbose version of the state variable'sset function. This was the avenue I pursued for a while. But it can get, well...ugly. Consider this:
constupdateMemberId=asyncuserValue=>{letlatestMemberId;awaitsetMemberId(userValue=>{latestMemberId=userValue;returnuserValue;});validateMemberId();if(validateEntireForm)validateForm();}
This... doesn't really solve much. On one hand, once we're past thesetMemberId()
logic, we have the latest-greatest value saved inlatestMemberId
. But wealready had that value saved inuserValue
and we'll still need to pass it into all of the downstream functions. Furthermore, we've started to litter up our logic withasync/await
- which is a problem when we have logic that shouldn'treally be asynchronous.
The Problem - Simplified
The "problem" I'm trying to highlight can be distilled down to this basic issue:
constsomeFunction=someValue=>{setSomeStateVariable(someValue);if(someConditionBasedOnSomeStateVariable){//...won't trigger based on the new value of 'someStateVariable'}callAFollowOnMethod();}constcallAFollowOnMethod=()=>{if(someStateVariable)//...won't recognize the latest value of 'someStateVariable'}
If we want to distill this into an evensimpler example, there are just some times when we really want to do something like this:
console.log(stateVariable);// 1setStateVariable(2);console.log(stateVariable);// 2setStateVariable(3);console.log(stateVariable);// 3
In other words,sometimes, you really need to update a state variable and know that, very soon thereafter, you can retrieve thelatest, mostup-to-date value, without worrying about asynchronous effects.
To be absolutely clear, I fully understand thatsome things will always be, andshould always be, asynchronous. For example, if you have three state variables that hold the responses that come back from three consecutive API calls, thenof course those values will be set asynchronously.
But when you have three state variables that are consecutively set with three simple scalar values - well... it can be kinda frustrating when those values aren't available to be readimmediately. In other words, if you can do this:
letfoo=1;console.log(foo);// 1foo=2;console.log(foo);// 2
Then it can be somewhat frustrating when you realize that you can't do this:
const[foo,setFoo]=useState(1);console.log(foo);// 1setFoo(2);console.log(foo);// 1
So... how do we address this???
Eureka(?)
Here's what I've been working with lately. It's dead-simple. No clever solution here. But it satisfies two of my main concerns:
I want to always have a way to retrieve theabsolute latest state value.
I'd really like to have the new state valuereturned to me after state updates. This may not seem like that big-of-a-deal - but sometimes, I really wish that the built-in
set()
functions would simply return the new value to me. (Of course, theycan't simply return the new value, because they're asynchronous. So all they could return would be a promise.)
To address these two issues, I created this (super crazy simple) Hook:
import{useState}from'react';exportdefaultfunctionuseTrait(initialValue){const[trait,updateTrait]=useState(initialValue);letcurrent=trait;constget=()=>current;constset=newValue=>{current=newValue;updateTrait(newValue);returncurrent;}return{get,set,}}
[NOTE: I'm not really sold on the name "trait". I only used it because I felt it was too confusing to call it some version of "state". And I didn't want to call the HookuseSynchronousState
because this isn't really synchronous. It just gives the illusion of synchronicity by employing a second tracking variable.]
This would get used like this:
constSomeComponent=()=>{constcounter=useTrait(0);constincrement=()=>{console.log('counter =',counter.get());// 0constnewValue=counter.set(counter.get()+1);console.log('newValue =',newValue);// 1console.log('counter =',counter.get());// 1}return(<>Counter:{counter.get()}<br/><buttononClick={increment}>Increment</button></>);return(<>UXHere...</>);}
This is a reasonable impersonation of synchronicity. By using two variables to track a single state value, we can reflect the change immediately by returning the value ofcurrent
. And we retain the ability to trigger re-renders because we're still using a traditional state variable inside the Hook.
Downsides
I don't pretend that this little custom Hook addresses all of the issues inherent in setting a state variable - and then immediately trying to retrieve thelatest value of that state variable. Here are a few of the objections I anticipate:
useTrait()
doesn't work if the value being saved is returned in atruly asynchronous manner. For example, if the variable is supposed to hold something that is returned from an API, then you won't be able to simplyset()
the value and then, on the very next line,get()
the proper value. This is only meant for variables that you wouldn't normally think of as being "asynchronous" - like when you're doing something dead-simple, such as saving a number or a string.It will always be at leastsomewhat inefficient. For every "trait" that's saved, there are essentiallytwo values being tracked. In the vast majority of code, trying to fix this "issue" would be a micro-optimization. But there are certainlysome bulky values that should not be chunked into memory twice, merely for the convenience of being able to immediately retrieve the result of
set()
operations.It's potentially non-idiomatic. As mentioned above, I'm fully aware that the Children of Redux would almost certainly address this issue with
useReducer()
. I'm not going to try to argue them off that cliff. Similarly, the Children of Hooks would probably try to address this withuseEffect()
. Personally, Ihate that approach, but I'm not trying to fight that Holy War here.I feel like I'm overlooking some simpler solution. I've done the requisite googling on this. I've read through a pile of StackOverflow threads. I haven'tgrokked any better approach yet. But this is one of those kinda problems where you just keep thinking that, "I gotta be overlooking some easier way..."
Top comments(16)

- LocationKansas City
- WorkWeb Developer 👌 at Bentley Systems
- Joined
I think you missed Standard (Poor) Solution #7:useCallback
constvalidateMemberId=useCallback(()=>{// validate based on the CURRENT value of 'memberId'// this function gets updated whenever memberId is updated,// so we know it will be the most recent id you just setreturnvalidOrNot;},[memberId]);
It has downsides because now you need to wrapvalidateForm
,updateMemberId
and on up through the call chain withuseCallback
as well. If you have the react-hooks lint plugin installed, it will warn you to do this; otherwise these functions can be re-created with each render.
I've been looking into Recoil lately for situations like this, but I haven't started testing it out yet so I don't have any good thoughts on if it is applicable. Seems a lot simpler than Redux, though!

- Email
- LocationNew Orleans, LA
- WorkFrontend Software Engineer
- Joined
Great feedback! I've onlyread aboutuseCallback()
. Haven't yet found a great time/place to use it. But you're right, this is definitely another one of the valid options.
As you've pointed out, it also has some drawbacks. And I don't personally know if I'd use it in this scenario (in place of my custom Hook). But I definitely think thatuseCallback()
is a better alternative thanuseEffect()
. And I'm thinking that, in some other scenarios whereuseEffect()
drives me nuts, it might be because I should really be usinguseCallback()
...

- LocationDijon, France
- Joined
Isn't one of your problems that you're not actually embracing the state concept? I mean, you have your form values in state, the validation result is directly derived from it, and rendering directly derived from both (you'll set your state into your form components' value, and conditionally display validation errors).
So, run validation each time you render, or if it's heavyweight, then use useMemo. If you can break down validation into smaller parts, each one in it's useMemo, then some can be skipped if their input (field value, or the result of another validation step) hasn't changed.
React is all about having a state and deriving rendering from it; and updating it when there's an event. So you can either run validation at the same time you update the state, to store validation result into the state as well; or run it at render time, memoizing its result.

- Email
- LocationNew Orleans, LA
- WorkFrontend Software Engineer
- Joined
You're not wrong. But the reply is also a bit narrow in scope (probably because the examples I gave were purposely over-simplified, to avoid making it a code-specific case study).
On one level, sure, we should always strive to utilize state in a manner consistent with your description. On another level, I think it's a bit naïve to imagine that we canalways do that. For example, there are times when the validation logic is sufficiently complex that I absolutely don'twant for it to run at render time. Because if you run it at render time, that means it's going to runon every re-render.
Of course, you make a good reference touseMemo()
. A tool like that can take the sting out of running something repeatedly on render. Admittedly, I need to get more comfortable with that valuable tool.
But I guess the deeper, simplified issue is this:
Setting a state variablefeels very similar to setting any other type of variable. Yeah, we know that it triggers some very different stuff in the background. But it's extremely common, inany language or style of coding, to set a variable on one line, and then, a handful of microseconds later, in some other block of code, check on the value in that same variable.
So the question becomes, if you have a scenario in React where you've set a state variable on one line, and then, a handful of microseconds later, in some other block of code, you need to check on the value in that same variable, how do you do that? As I've tried to point out in this article, the answer to that question can be quite tricky.

- LocationDijon, France
- Joined
So the question becomes, if you have a scenario in React where you've set a state variable on one line, and then, a handful of microseconds later, in some other block of code, you need to check on the value in that same variable, how do you do that?
Well, I would say you try hardnot to get into that situation. And I believe there are ways to avoid that situation that would also be more idiomatic; by thinking differently about your problem.
I think the crux is to definitely stop thinking aboutevents and "when you set X" or "when you change state".
If you need to compute state that depends on other state, then try storing coarser values into state (your whole form as one object) and pass a function to the state setter, or deriving at rendering time and memoizing.

- LocationSydney, Australia
- WorkPrincipal Engineer at Atlassian (ex. Pearson, TripAdvisor, Intel)
- Joined
Just a small point about states vs. refs. Not sure how helpful in your case (need to see the JSX).
I see devs reaching out to useState for every variable they want to store (because the functional componenet runs everything from the top every time and normal variables don't persist).
The thing is, states are tied to React's rendering cycle. In other words, only use states when the desired effect of changing the value is a re-render (applies to using states within custom hooks as well).
If all you need is a variable that persists between renders but doesn't need to trigger re renders, a ref is the way to go (and as you mentioned it updates like a normal variable because it is).

- Email
- LocationNew Orleans, LA
- WorkFrontend Software Engineer
- Joined
Excellent point!

- LocationEdinburgh, Scotland
- EducationBS in CS from UTD
- WorkEM / Staff Engineer at Administrate
- Joined
I think Vue's state pattern ia basically your custom hook but 'directly' on the trait; ie the getter/setter is how you access and set things in Vue. I think this is how that new hotness framework works... Uh... Hrmm.. svelte! That's the one. In vue (and knockout) i think there's a concept called a computed prop that is also similar to this problem. However, in knockout at least, it was limited such that you couldn't make cycles.
Could this also be solved by just making the validations also async?

- Email
- LocationNew Orleans, LA
- WorkFrontend Software Engineer
- Joined
One of my previous approaches was making the validations async. That... worked. But I don't consider it to be ideal. Not that I have any problem with the concept ofasync/await
, but it's kinda like a virus that spreads beyond its originally-intended borders, cuzawait
must be insideasync
. And once you make a functionasync
, it returns a promise, that (depending upon your project and your inspections) eithershould ormust be handled. Which too often leads to making the callerasync
, which in turn leads to the caller's caller being madeasync
...
Before you know it, every dang function isasync
- which makes everything kinda messy for no good reason.

- LocationEdinburgh, Scotland
- EducationBS in CS from UTD
- WorkEM / Staff Engineer at Administrate
- Joined
Agreed.

- Email
- LocationNew Orleans, LA
- WorkFrontend Software Engineer
- Joined
Thank you - I'm so glad that it helped!

exportconstuseSyncState=<T>(initialState:T):[()=>T,(newState:T)=>void]=>{const[state,updateState]=useState(initialState)letcurrent=stateconstget=():T=>currentconstset=(newState:T)=>{current=newStateupdateState(newState)returncurrent}return[get,set]}
A generic variant for TypeScript if anybody is interested.

- Email
- LocationNew Orleans, LA
- WorkFrontend Software Engineer
- Joined
Awesome - thank you!
For further actions, you may consider blocking this person and/orreporting abuse