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
3.react source code architecture
4. Source directory structure and debugging