Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

State update being overridden by another (previous) state update?

I know this seems as unusual example, but still I can't seem to explain precisely why do I never see valueB printed on console after I click the div?

Note that since I am calling the two set state calls in a setTimeout, they are not batched.

function App() {
  let [a, setA] = React.useState();
  let [b, setB] = React.useState();

  React.useEffect(() => {
    console.log('Entering useEffect', a, b);

    return () => {
      console.log('Entering cleanup', a, b);

      setA(null);
      setB(null);
    };
  }, [a, b]);

  console.log('Render', a, b);

  return (
    <div
      onClick={() => {
        setTimeout(() => {
          setA('valueA');
          setB('valueB');
        }, 100);
      }}
    >
      <h1>Test App</h1>
    </div>
  );
}

ReactDOM.render(
  <App/>,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
like image 493
Giorgi Moniava Avatar asked Jun 22 '21 08:06

Giorgi Moniava


Video Answer


1 Answers

Normally useEffect and its cleanup are called asynchronously (on another stack) after render. But if you call setState in a timer and there are useEffect callbacks, they are invoked eagerly but all accumulated state change from cleanup is called after setState that invoked this operation.

So in your example when setTimeout handler is called:

  • when calling setA: a state is changed and two state changes from cleanup are added to pending state change queue
  • when calling setB: first valueB is applied to b and then null from the cleanup (it's kind of batched here)

This tries to imitate the behaviour when state updates are actually batched like in click handlers (first apply state updates from the click handler and then from useEffect).

You can see what's happening more clearly when you use updater functions:

function App() {
  Promise.resolve().then(() => console.log("**** another stack ****"));
  console.log("before useStateA");
  let [a, setA] = React.useState();
    console.log("between useStates");
  let [b, setB] = React.useState();
    console.log("after useStateB");
  

  React.useEffect(() => {
    console.log('Entering useEffect', a, b);

    return () => {
      console.log('Entering cleanup', a, b);

      setA(() => (console.log("setting a to null from cleanup"), null));
      setB(() => (console.log("setting b to null from cleanup"), null));
    };
  }, [a, b]);

  console.log('Render', a, b);

  return (
    <div
      onClick={() => {
        setTimeout(() => {
          console.log("****timer start****");
          setA(() => (console.log("setting a to valueA from timer"), "valueA"));
          console.log("between timer setters");
          setB(() => (console.log("setting b to valueB from timer"), "valueB"));
          console.log("****timer end****");
        }, 100);
      }}
    >
      <h1>Test App</h1>
    </div>
  );
}

ReactDOM.render(
  <App/>,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
like image 141
marzelin Avatar answered Oct 22 '22 12:10

marzelin