[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-synch...
For further actions, you may consider blocking this person and/or reporting abuse
I think you missed Standard (Poor) Solution #7:
useCallback
It has downsides because now you need to wrap
validateForm
,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!
Great feedback! I've only read about
useCallback()
. 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 that
useCallback()
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()
...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.
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 can always do that. For example, there are times when the validation logic is sufficiently complex that I absolutely don't want for it to run at render time. Because if you run it at render time, that means it's going to run on every re-render.
Of course, you make a good reference to
useMemo()
. 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 variable feels 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, in any 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.
Well, I would say you try hard not 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 about events 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.
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).
Excellent point!
Regarding useRef, I have this thing, even though I'm completely bypassing React state and just using it as a kind of forceUpdate.
gist.github.com/drodsou/b947eb192d...
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?
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 of
async/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) either should or must 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 is
async
- which makes everything kinda messy for no good reason.Agreed.
Great article! Helped me solve a problem here now and saved me from a lot of headache.
Thank you - I'm so glad that it helped!
A generic variant for TypeScript if anybody is interested.
Awesome - thank you!
Thank you for writing about this problem in detail. And also thank you for creating this NPM package, you made my day!