Interviewer: is the setState in react synchronous or asynchronous

Posted by spikeon on Wed, 15 Dec 2021 05:28:38 +0100

Interviewer: is the setState in react synchronous or asynchronous

hello, this is Xiaochen. Do you often encounter such questions during the interview? Is the setState of react synchronous or asynchronous? This question must be answered completely. Let's take a look at the following examples:

Example 1: click button to trigger update, and setState will be called twice in the handle function

export default class App extends React.Component {
  state = {
    num: 0,
  };
  updateNum = () => {
    console.log("before", this.state.num);

    this.setState({ num: this.state.num + 1 });
    this.setState({ num: this.state.num + 1 });
    console.log("after", this.state.num);

  };
  render() {
    const { num } = this.state;
    console.log("render", num);
    return <button onClick={this.updateNum}>hello {num}</button>;
  }
}

//Print results
//render     0
//before     0
//after     0
//render     1

Example 2: the two setstates of example 1 are executed in the setTimeout callback

export default class App extends React.Component {
  state = {
    num: 0,
  };
  updateNum = () => {
    console.log("before", this.state.num);

    setTimeout(() => {
        this.setState({ num: this.state.num + 1 });
        this.setState({ num: this.state.num + 1 });
        console.log("after", this.state.num);
    }, 0);
  };
  render() {
    const { num } = this.state;
    console.log("render", num);
    return <button onClick={this.updateNum}>hello {num}</button>;
  }
}

//Print results
//render     0
//before     0
//render     1
//render     2
//after     2

Example 3: use unstable_batchedUpdates is executed in setTimout, which is unstable_ The callback function in batchedUpdates calls two times setState.

import { unstable_batchedUpdates } from "react-dom";

export default class App extends React.Component {
  state = {
    num: 0,
  };
  updateNum = () => {
    console.log("before", this.state.num);

    setTimeout(() => {
      unstable_batchedUpdates(()=>{
        this.setState({ num: this.state.num + 1 });
        this.setState({ num: this.state.num + 1 });
        console.log("after", this.state.num);
      })
    }, 0);
  };
  render() {
    const { num } = this.state;
    console.log("render", num);
    return <button onClick={this.updateNum}>hello {num}</button>;
  }
}

//Print results
//render     0
//before     0
//after     0
//render     1

Example 4: setState is executed twice in the setTimeout callback, but it is started in concurrent mode, that is, reactdom is called unstable_ Createroot starts the application.

import React from "react";
import ReactDOM from "react-dom";

export default class App extends React.Component {
  state = {
    num: 0,
  };
  updateNum = () => {
    console.log("before", this.state.num);

    setTimeout(() => {
        this.setState({ num: this.state.num + 1 });
        this.setState({ num: this.state.num + 1 });
        console.log("after", this.state.num);
    }, 0);
  };
  render() {
    const { num } = this.state;
    console.log("render", num);
    return <button onClick={this.updateNum}>hello {num}</button>;
  }
}

ReactDOM.unstable_createRoot(rootEl).render(<App />);
                                            
//Print results
//render     0
//before     0
//after     0
//render     1

batchedUpdates

Simply put, multiple updates are triggered simultaneously in a context, and these updates are combined into one update, such as

onClick() {
  this.setState({ count: this.state.count + 1 });
  this.setState({ count: this.state.count + 1 });
}

In the previous react version, if it is separated from the current context, it will not be merged. For example, multiple updates are placed in the setTimeout because the executionContext of multiple setstates in the same context will contain BatchedContext, and the setstates containing BatchedContext will be merged. When the executionContext is equal to NoContext, The tasks in SyncCallbackQueue will be executed synchronously, so multiple setstates in setTimeout will not be merged and will be executed synchronously.

onClick() {
 setTimeout(() => {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
  });
}
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      resetRenderTimer();
       //When the executionContext is NoContext, the tasks in SyncCallbackQueue are executed synchronously
      flushSyncCallbackQueue();
    }
  }
}

In Concurrent mode, the above examples will also be merged into one update. The root cause is in the following simplified source code. If you have multiple setState callbacks, you will compare the priorities of these setState callbacks. If the priorities are the same, you will return first and will not carry out the subsequent render phase

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;//Callback of setState that has been called before
  //...
    if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    //If the callback priority of the new setState is equal to that of the previous setState, it will enter the logic of batchedUpdate
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    cancelCallback(existingCallbackNode);
  }
    //Starting point of scheduling render phase
    newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root),
  );
    //...
}

Then why do the setTimeout callbacks have the same priority of setState multiple times in Concurrent mode? Because in the function requestUpdateLane for obtaining lane, only the first setState satisfies currentEventWipLanes === NoLanes, so their currentEventWipLanes parameters are the same, In findUpdateLane, the schedulerLanePriority parameter is the same (the scheduling priority is the same), so the returned lane is the same.

export function requestUpdateLane(fiber: Fiber): Lane {
    //...
  if (currentEventWipLanes === NoLanes) {//The first setState satisfies currentEventWipLanes === NoLanes
    currentEventWipLanes = workInProgressRootIncludedLanes;
  }
  //...
  //In setTimeout, schedulerlanepriority and currenteventwiplanes are the same, so the returned lane is also the same
  lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
  //...

  return lane;
}

Summary:

In legacy mode: hit batchupdates asynchronously and miss batchupdates synchronously

In concurrent mode: all are asynchronous

Video Explanation (efficient learning): Click to learn

Previous react source code analysis articles:

1. Introduction and interview questions

2. Design concept of react

3.react source code architecture

4. Source directory structure and debugging

5. JSX & Core api

6.legacy and concurrent mode entry functions

7.Fiber architecture

8.render stage

9.diff algorithm

10.commit phase

11. Life cycle

12. Status update process

13.hooks source code

14. Handwritten hooks

15.scheduler&Lane

16.concurrent mode

17.context

18 event system

19. Handwritten Mini react

20. Summary & answers to interview questions in Chapter 1

Topics: React