Here is the thing, React is a WTF framework that has a steep learning curve yet powerful in the frontend development. Last time, I have written about this blog about react hooks. The more I use react in my day job projects, the more I can dig out new to refresh my perspective.

The problem in this time makes me conclude that react itself is almost asynchronous in every aspect and in its utilities and its design idea.

My goal is simpler. I work in the ticket context which has many states (useState hook) and callbacks (useEffect hook) are exposed for its potential consumers. And the context also has a bunch of effects (useEffect hook). As far as I know useEffect hook is used for kind of things like subscribers to states, lifecycle functions on a react component/context. Back to my goal, I want to have boolean flag that can indicate whether a callback is called, then set to true, otherwise it is false initially and the context somehow is reset. So I create something like

const [flag, setFlag] = useState(false);

// In callback is called
...
setFlag(true);
...

// In a useEffect hook that does the reset I think of
...
setFlag(false)
...

// Somewhere in the code I want to test the flag value to do thing
...
if (flag) {
    // Do something when flag === true
} else{
    // Do something else when flag === false
}

Battling with React

However, I can see the inconsistent outcome where the setFlag(true) does run, but the flag is not set to true in the if-else block. Worse thing is that when I click more times from UI, probably the second time or the third time, flag is set to true. But that has already messed up the audit logs etc. I can’t accept this as a done.

I guess that incorrectly set the deps array of useEffect hook can lead the callback to be triggered more than you want (or less than you want). I am trying to tweak multiple combination of the state inside/outside of the deps array of the useEffect hook, or in another new useEffect hook etc. TBH, I can’t quite remember the combinations I have tried. At least I can conclude that the elements in the deps array is OR relationship.

It’s painful to deal with this kind of issue. I begin to question Gihub Copilot from the other angle because the chat suggests me to think useEffect and useState direction (I know that the AI mislead me now). I try to ask it that “I want the state change to be synchronous for example, setFlag(true), the if (flag) the flag must be true sth like that”. I’ve known React could set the state in its painting lifecycle, which is hidden from us and is only controlled by React rendering engine. Surprisingly, the Copilot this time suggests me to try useRef, which it says useRef is synchronous as I wish. Hence, I replace the useState with useRef, abandon mutation of true/false in the useEffect hook. Finally that works!

I’ve Learned

useState is asynchronous and controlled by the React internal painting engine (maybe it is called like that). The state value is not deterministic where it could be changed to something else by React cycle runs, or your expected value is not yet evaluated while the React cycle does not yet run. See its caveats. Some key points: only update in next render, use Object.is to determine state change or not, React batches state updates, which means the state could possibly not be updated in time balabalabala…

useRef that lets you reference a value that’s not needed for rendering. The very first statement in the doc is already promising. See its caveats. Strictly speaking, useRef is not simply considered as synchronous, instead useRef is excluded from React’s rendering and React acts asynchronously, that makes useRef acts in sync manner or suitable in sync scenario. You can get what you change if using useRef.

One caveat I learnt from Copilot is that if you want to mutate the state behavior out of the React rendering cycle, you can move out state’s value to useRef.

WTF React

A few blames I can moan:

  • The name useState and useRef do not convey one can be affected by the React rendering but one not.
  • Should I read all React’s document? They are a lot…
  • Probably I could be more sensitive because get value from useState and useRef is different where aRef.current if using useRef. That warns you they are different.
  • Framework is also a double-edged thing. React wants to internally do more optimization for developers so it has to hide things for you with some poor naming hooks.
  • Svelte may implement this functionality better. Like reactive statement $: then you know it is doing non-deterministic thing out there.