reactjsevent-loop

Priority of react's useEffect in Event-Loop


Background

Question

import { useEffect, useState } from 'react'

function App() {
  const [state, setState] = useState(0);
  console.log(1);


  queueMicrotask(() => {
    console.log('inside queueMicrotask - 1');
  });

  useEffect(() => {
    console.log(2);
  }, [state]);

  queueMicrotask(() => {
    console.log('inside queueMicrotask - 2');
  });
  
  Promise.resolve().then(() => {
    console.log(3);
  });

  setTimeout(() => {
    console.log(4);
  }, 0);

  const onClick = () => {
    console.log(5);
    setState((num) => num + 1);
    console.log(6);
  };

  return (
    <div>
      <button onClick={onClick} data-testid="action">
        click me
      </button>
    </div>
  );
}

export default App

The result is

1 2 inside queueMicrotask - 1 inside queueMicrotask - 2 3 4

I expected

1 inside queueMicrotask - 1 2 inside queueMicrotask - 2 3 4

How is it possible for the callback function of useEffect to be called before the callback of queueMicrotask? I was under the impression that the microtask queue has a very high priority in the Event Loop.


Solution

  • I was under the impression that the microtask queue has a very high priority in the Event Loop.

    It's misleading to think that microtasks have any priority, the microtask queue is simply visited as soon as the JS call-stack is empty, even from inside a single task. They don't participate at all in the task-prioritization system.

    So these results mean that the callback of useEffect is called "synchronously", before the main script ends.

    So basically you have the same as the below snippet:

    function App() {
      console.log("## begin App");
      queueMicrotask(() => console.log("microtask"));
      useEffect(() => console.log("effect"));
      console.log("## end App");
    }
    const effects = [];
    function useEffect(cb) {
      effects.push(cb);
    }
    function render(elem) {
      for (const effect of effects) {
        effect();
      }
    }
    
    function main() {
      console.log("#### begin main");
      const elem = App();
      render(elem); // actually synchronous
      console.log("#### end main");
    }
    
    main();

    This can be verified by adding some debugger statement and see that both the App function and the useEffect callback share a same synchronous caller in their call-stack.