search

Home  >  Q&A  >  body text

React18 microtask batch processing problem

useEffect(() => {
  console.log("render");
});

const handleClick = () => {
  setC1((c) => c + 1);

  Promise.resolve().then(() => {
    setC1((c) => c + 1);
  });
};

const handleClick2 = () => {
  Promise.resolve().then(() => {
    setC1((c) => c + 1);
  });

  setC1((c) => c + 1);
};

In the React18 version, why does clicking the handleClick method cause two renderings, while clicking the handleClick2 method only causes one rendering?

I want the output of both methods to be the same. Can anyone tell me why they are different?

P粉506963842P粉506963842489 days ago530

reply all(1)I'll reply

  • P粉642920522

    P粉6429205222023-09-08 17:06:21

    I will explain how these call sequences differ and how the observed behavior is possible.

    I can’t tell you exactly how React updates status in batches internally, I just assume that React has complex optimizations that are irrelevant to the developer using React and require a deep understanding of React internals and maybe even changing from one version to another. (Please feel free to correct me.)

    the difference

    Promise.resolve() Arranges a new microtask, which is actually equivalent to window.queueMicrotask().

    setState Function (possibly) will also schedule a new microtask, Therefore their callbacks (Promise and setState) are called in the same execution phase.

    The difference between these two variants is

    • In handleClickA, the setState2 hook is called between the two updater functions, while
    • In handleClickB, the two updater functions will be called directly in sequence.

    Sample code

    I rewrote your code slightly to better illustrate the calling sequence:

    const setState1 = setState;     
    const setState2 = setState;
    const update1 = updaterFunction; // c => c + 1
    const update2 = updaterFunction; // c => c + 1
    
    const handleClickA = () => {          
                                      // Scheduled functions:
        setState1( update1 );         // 1. --> [ update1 ]
        
        queueMicrotask(() => {        // 2. --> [ update1, setState2 ]
            setState2( update2 );     // 4. --> [ update2 ]
        });
    
        // update1();                 // 3. --> [ setState2 ]
        // setState2( update2 );      // 4. --> [ update2 ]
        // update2();                 // 5. --> []
    };
    
    const handleClickB = () => {
                                      // Scheduled functions:    
        queueMicrotask(() => {        // 1. --> [ setState2 ]
            setState2( update2 );     // 3. --> [ update2 ]
        });
    
        setState1( update1 );         // 2. --> [ setState2, update1 ]
        
        // setState2( update2 );      // 3. --> [ update1, update2 ]
        // update1();                 // 4. --> [ update2 ]
        // update2();                 // 5. --> []
    };

    Call sequence description

    Here I explain the calling sequence.

    (FIFO >):

    handleClickA

    // 0. --> []
    - schedule update1 (setState1())  // 1. --> [ update1 ]
    - schedule setState2              // 2. --> [ update1, setState2 ]
    - invoke update1()                // 3. --> [ setState2 ]
    - schedule update2 (setState2())  // 4. --> [ update2 ]
    - invoke update2()                // 5. --> []

    handleClickB

    // 0. --> []
    schedule setState2              // 1. --> [ setState2 ]
    schedule update1 (setState1())  // 2. --> [ setState2, update1 ]
    schedule update2 (setState2())  // 3. --> [ update1, update2 ]
    invoke update1()                // 4. --> [ update2 ]
    invoke update2()                // 5. --> []

    Personal interpretation

    I assume React attempts to batch all updater functions currently queued.

    i.e. whenever only the updater function is called, try batching them together and only update the final state once.

    However, if a new setState function is called, React may complete the current update loop and start a new render before calling the next updater periodic function.

    I can only guess why this is done

      Because the new
    • setState might somehow break the batch, or
    • If new
    • setState calls are made recursively, the next render will be delayed too much, or
    • React people are still figuring out the best optimization strategies and their trade-offs.
    • (...or this is a bug.)
    • reply
      0
  • Cancelreply