DEV Community

Cover image for Synchronous State With React Hooks

Synchronous State With React Hooks

Adam Nathaniel Davis on December 22, 2020

[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...
Collapse
 
havespacesuit profile image
Eric Sundquist

I think you missed Standard (Poor) Solution #7: useCallback

const validateMemberId = 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 set
    return validOrNot;
}, [memberId]);
Enter fullscreen mode Exit fullscreen mode

It has downsides because now you need to wrap validateForm, updateMemberId and on up through the call chain with useCallback 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!

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

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 than useEffect(). And I'm thinking that, in some other scenarios where useEffect() drives me nuts, it might be because I should really be using useCallback()...

Collapse
 
tbroyer profile image
Thomas Broyer

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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

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.

Collapse
 
tbroyer profile image
Thomas Broyer

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 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.

Collapse
 
isaachagoel profile image
Isaac Hagoel

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).

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Excellent point!

Collapse
 
drodsou profile image
drodsou

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...

Collapse
 
sirseanofloxley profile image
Sean Allin Newell • Edited

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?

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

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, cuz await must be inside async. And once you make a function async, 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 caller async, which in turn leads to the caller's caller being made async...

Before you know it, every dang function is async - which makes everything kinda messy for no good reason.

Collapse
 
sirseanofloxley profile image
Sean Allin Newell

Agreed.

Collapse
 
lazee profile image
Jakob Vad Nielsen

Great article! Helped me solve a problem here now and saved me from a lot of headache.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Thank you - I'm so glad that it helped!

Collapse
 
lazee profile image
Jakob Vad Nielsen
export const useSyncState = <T> (initialState: T): [() => T, (newState: T) => void] => {
    const [state, updateState] = useState(initialState)
    let current = state
    const get = (): T => current
    const set = (newState: T) => {
        current = newState
        updateState(newState)
        return current
    }
    return [get, set]
}
Enter fullscreen mode Exit fullscreen mode

A generic variant for TypeScript if anybody is interested.

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

Awesome - thank you!

Collapse
 
nxcco profile image
Nico

Thank you for writing about this problem in detail. And also thank you for creating this NPM package, you made my day!