1

Because of the asynchronous nature of React setState and the newly introduced react concurrent mode after a lot of research, I don't know how I can access the guaranteed latest state in the following scenario or any other scenario.

An answer from a react expert especially from the React team is appreciated so much.

Please remember it's a so simplified example and the real problem may occur in a sophisticated project full of state updates, event handlers, async code that changes state, ...

import { useCallback, useState } from "react";

const Example = ({ onIncrement }) => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    onIncrement(count, count + 1);  // Is count guaranteed to be the latest state here due to including count in the useCallback dependency array?
    setCount((count) => count + 1);
  }, [count, onIncrement]);

  return (
    <>
      <span>{count}</span>
      <button onClick={increment}>increment</button>
    </>
  );
};

const Parent = () => (
  <Example
    onIncrement={(currentCount, incrementedCount) =>
      console.log(
        `count before incrementing: ${currentCount}, after increment: ${incrementedCount}`
      )
    }
  />
);

export default Parent;

You may say I could call onIncrement in the setCount callback, but due to the new react concurrent mode you can see that in future react updates the onIncrement may be called twice and output the result twice which is not the desired result.

The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming concurrent mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

You can already (React 17 strict mode) see that onIncrement will be called twice in development mode and in the feature after React concurrent mode becomes the default, so it may be called twice this way in production.

Does including count in the useCallback dependencies array guaranty that count is the latest state value?

You may suggest calling the onIncrement in a useEffect hook but that way I will not have access to the previous state, which unlike this example may be impossible to recalculate. (using a ref for storing the previous state is not a solution)

Another problem with using useEffect is that I don't know for sure that this event handler (onIncrement) is the cause of the effect or has state change in another handler or useEffect callback caused the effect. (storing an extra state or ref for detecting the cause is overkill)

Thank you!

Abbas Hosseini
  • 1,545
  • 8
  • 21
  • Why can't you call onIncrement inside the setCount callback? – Jonas Wilms Mar 02 '21 at 22:46
  • It may be called twice in production due to the new concurrent mode as explained in the answer and it will be called twice in strict mode to avoid future errors. – Abbas Hosseini Mar 02 '21 at 22:54
  • Ah sorry, missread that part and actually assumed this was not the case. Seems like React is evolving completely again ... You say that "using a ref for storing the previous state is not a solution", but in fact that sounds like a good idea? What's your reason for not wanting a ref? – Jonas Wilms Mar 02 '21 at 23:10
  • Sorry for responding late. The problem is that if I choose to store the previous state in a ref then I'll have to call `onIncrement` in a `useEffect` callback for accessing the latest state, and I will have to use another state or ref for detecting that the click event is the cause of the effect and not another event or code that changes the count state. And when you have so many code like this one, you're code will be polluted with extra useState, useRef and useEffect hooks. – Abbas Hosseini Mar 03 '21 at 11:03
  • As the `increment` function is triggered by a user event, I'd say that `count` is the _latest value the user saw_ when clicking the button. But I see that there are cases when the "latest value" instead of the "latest rendered value" is needed, and in that case all combinations of the "known hooks" are not suitable. Maybe this is worth raising an issue to the React team, cause I think this could only be solved through a new "ref state" – Jonas Wilms Mar 03 '21 at 11:42
  • I searched React issues and found similar issues raised up even before this new concurrent mode, like this one https://github.com/facebook/react/issues/14092 and this one which talks about concurrent mode too https://github.com/facebook/react/issues/14092 , but all of the solutions seem hacky to me. Let me know if you found a reasonable solution or a best practice. Thank you! – Abbas Hosseini Mar 03 '21 at 11:54

1 Answers1

1

The source of my confusion was articles that said react may postpone state updates due to heavy workload and that you shouldn't rely on the state value outside of setState callback. After a lot of research and according to this answer, now I know that react may batch state changes but it always keeps the order.

If I've understood well, React will invoke the render phase after each state change batch, like event handlers in my case, and for sure it will invoke render phases in order but may choose to postpone them or not to commit them in concurrent mode.

The other source of my confusion was the below quote from 'React docs':

The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming concurrent mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

which I had misunderstood and I thought that React might choose to ignore invoking some render phase completely, but the reality is that react will invoke it but might choose not to commit it.

So fortunately the fix for this is tiny:

const incrementHandler = () => {
  const newCount = count + 1;

  // Event handlers can have side effects!
  // Calling onIncrement here even has an added benefit:
  // If onIncrement also updates state, the updates will get batched by React — which is faster!
  onIncrement(count, newCount);

  // You can also use the simpler updater form of just passing the new value in this case.
  setCount(newCount);
};

The above code is the answer from Brian Vaughn replying to me rising an issue in React's github repo. https://github.com/facebook/react/issues/20924

The other answer from Dan Abramov which is so clarifying:

I think one crucial nuance we're overlooking here is React will not actually do any of that. In particular, React does offer a guarantee that you will not get a render of the "next click" before the "previous click" has been flushed. This has already been a guarantee with Concurrent Mode, but we're making it even a stronger one — click events (and similar "intentional" user event like keyboard input) will always flush without interruption in a microtask. In practice, this means events for clicks and other intentional events will never get "ignored" as in this hypothetical scenario we're discussing. We'll include this in the documentation when it's stable.

Thank you Dan and Brian

Abbas Hosseini
  • 1,545
  • 8
  • 21
  • This thread also inspired my issue on the documentation issuetracker (https://github.com/reactjs/reactjs.org/issues/3560) ... – Jonas Wilms Mar 03 '21 at 23:08
  • I think gaearons comment 'This has already been a guarantee with Concurrent Mode, but we're making it even a stronger one — click events (and similar "intentional" user event like keyboard input) will always flush without interruption in a microtask.' is actually the missing piece to the whole story. – Jonas Wilms Mar 03 '21 at 23:12