SetState is synchronous or asynchronous

Custom synthesized events and react hook functions update state asynchronously

Take setState in a custom click event

import React, { Component } from 'react';
class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
    console.log(this.state.count);
  }
  render() {
    return (
      <div style={{ width: '100px', height: '100px', backgroundColor: "yellow" }}>
          {this.state.count}
      </div>
    )
  }
}
export default Test;
Copy the code

Click once and this.state.count will print 1 and the page will display 2. According to the phenomenon, the three setStates are only the last setState to take effect, and the first two setStates have no effect. Because if you change setState to +3 the first time, count prints to 1, and displays to 2, it doesn’t change. And there is no synchronization to get the result of count.

At this point, we can adjust the code to get the updated state by passing the second argument to setState:

import React, { Component } from 'react';
class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }
  handleClick = () => {
    this.setState({
      count: this.state.count + 3
    }, () => {
      console.log('1', this.state.count)
    });
    this.setState({
      count: this.state.count + 1
    }, () => {
      console.log('2', this.state.count);
    });
    this.setState({
      count: this.state.count + 1
    }, () => {
      console.log('3', this.state.count);
    });
    console.log(this.state.count);
  }
  render() {
    return (
      <div style={{ width: '100px', height: '100px', backgroundColor: "yellow" }}>
          {this.state.count}
      </div>
    )
  }
}
export default Test;
Copy the code

At this point, click once, and the three setState callback functions will print the result.

1, 1: 2, 2: 2, 3: 2Copy the code

First, the last line prints 1 directly. Then, in the setState callback, the results are printed with the most recently updated 2. Although the first two sets of setState did not take effect, they still print a 2 in the second argument.

At this point, replace the first parameter of setState with a function, and the state before the update can be obtained through the first parameter of the function.

import React, { Component } from 'react';
class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }
  handleClick = () => {
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    });
    console.log(this.state.count);
  }
  render() {
    return (
      <div style={{ width: '100px', height: '100px', backgroundColor: "yellow" }}>
          {this.state.count}
      </div>
    )
  }
}
export default Test;
Copy the code

At this point, the printed result is 1, but the page displays a count of 4. It can be found that if setState updates state in the way of passing parameters, several times of setState will not only update the last time, but several times of updating state will take effect.

Let’s see what count is printed in the second function:

import React, { Component } from 'react';
class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }
  handleClick = () => {
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('1', this.state.count);
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('2', this.state.count);
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('3', this.state.count);
    });
    console.log(this.state.count);
  }
  render() {
    return (
      <div style={{ width: '100px', height: '100px', backgroundColor: "yellow" }}>
          {this.state.count}
      </div>
    )
  }
}
export default Test;
Copy the code

At this point, click once, the three setState callback functions, the print result is as follows, as you can imagine, the page display result is also 4

1, 1: 4, 2: 4, 3: 4Copy the code

Put the above code into something like componentDidMount and the output is the same.

As you can see, state updates are asynchronous in custom composite events and hook functions.

State is updated synchronously in native events and setTimeout

Take setState in setTimeout

import React, { Component } from 'react';
class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1
      }, () => {
        console.log('1:', this.state.count);
      });
      this.setState({
        count: this.state.count + 1
      }, () => {
        console.log('2:', this.state.count);
      });
      this.setState({
        count: this.state.count + 1
      }, () => {
        console.log('3:', this.state.count);
      });
      console.log(this.state.count);
    }, 0);
  }
  render() {
    return (
      <div 
        style={{ 
          width: '100px', 
          height: '100px', 
          backgroundColor: "yellow" 
        }}>
          {this.state.count}
      </div>
    )
  }
}
export default Test;
Copy the code

At this point, the output is as follows:

1: 2
2: 3
3: 4
4
Copy the code

Replace the first argument to setState with a function:

componentDidMount() {
  setTimeout(() => {
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('1', this.state.count);
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('2', this.state.count);
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('3', this.state.count);
    });
    console.log(this.state.count);
  }, 0);
}
Copy the code

The printed result is the same as above.

Is there a feeling that state is completely controllable? In setTimeout, setState will take effect multiple times, and the updated state can be obtained in the second parameter of each setState.

Similarly, the output in the native event is the same as in setTimeout and is synchronous.

import React, { Component } from 'react';
class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 1
    };
  }
  componentDidMount() {
    document.body.addEventListener('click', this.handleClick, false);
  }
  componentWillUnmount() {
    document.body.removeEventListener('click', this.handleClick, false);
  }
  handleClick = () => {
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('1', this.state.count);
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('2', this.state.count);
    });
    this.setState((prevState, props) => {
      return { count: prevState.count + 1 }
    }, () => {
      console.log('3', this.state.count);
    });
    console.log(this.state.count);
  }
  render() {
    return (
      <div
        style={{ 
          width: '100px', 
          height: '100px', 
          backgroundColor: "yellow" 
        }}
      >
        {this.state.count}
      </div>
    )
  }
}
export default Test;
Copy the code

SetState related source code

The following code is from act17.0.2

Directory/packages/react/SRC/ReactBaseClasses. Js

function Component(props, context, updater) { this.props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; } Component.prototype.isReactComponent = {}; Component.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...) : takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); this.updater.enqueueSetState(this, partialState, callback, 'setState'); };Copy the code

SetState can take two arguments, the first of which can be object, function, and null. Undefined will not throw an error. To perform this. At the bottom of the updater. EnqueueSetState method. Look globally for enqueueSetState and find two sets of directories with this variable.

First, the first set of directories:

Directory/packages/react/SRC/ReactNoopUpdateQueue js line 100 enqueueSetState method, parameter this respectively, and the initial state, the callback, and string setState, This refers to the current React instance.

enqueueSetState: function(
  publicInstance,
  partialState,
  callback,
  callerName,
) {
  warnNoop(publicInstance, 'setState');
}
Copy the code

Moving on to the warnNoop method:

const didWarnStateUpdateForUnmountedComponent = {};

function warnNoop(publicInstance, callerName) {
  if (__DEV__) {
    const constructor = publicInstance.constructor;
    const componentName =
      (constructor && (constructor.displayName || constructor.name)) ||
      'ReactClass';
    const warningKey = `${componentName}.${callerName}`;
    if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
      return;
    }
    console.error(
      "Can't call %s on a component that is not yet mounted. " +
        'This is a no-op, but it might indicate a bug in your application. ' +
        'Instead, assign to `this.state` directly or define a `state = {};` ' +
        'class property with the desired state in the %s component.',
      callerName,
      componentName,
    );
    didWarnStateUpdateForUnmountedComponent[warningKey] = true;
  }
}
Copy the code

This code is equivalent to didWarnStateUpdateForUnmountedComponent objects with properties, properties of the key for the React to current setState components. SetState, if the current this attribute is returned; Sets the value of this property to true if it does not currently exist or if the value is false.

Let’s go to another directory:

Directory/react – the reconciler/SRC/ReactFiberClassComponent. New. Js and ReactFiberClassComponent. Old. Js

const classComponentUpdater = { enqueueSetState(inst, payload, callback) { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); const update = createUpdate(eventTime, lane); update.payload = payload; if (callback ! == undefined && callback ! == null) { if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update, lane); const root = scheduleUpdateOnFiber(fiber, lane, eventTime); if (root ! == null) { entangleTransitions(root, fiber, lane); } if (__DEV__) { if (enableDebugTracing) { if (fiber.mode & DebugTracingMode) { const name = getComponentNameFromFiber(fiber) || 'Unknown'; logStateUpdateScheduled(name, lane, payload); } } } if (enableSchedulingProfiler) { markStateUpdateScheduled(fiber, lane); }}}Copy the code

The main focus is the enqueueUpdate function

Directory/react – the reconciler/SRC/ReactUpdateQueue. New. Js and ReactUpdateQueue. Old. Js

export function enqueueUpdate<State>( fiber: Fiber, update: Update<State>, lane: Lane, ) { const updateQueue = fiber.updateQueue; if (updateQueue === null) { // Only occurs if the fiber has been unmounted. return; } const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; if (isInterleavedUpdate(fiber, lane)) { const interleaved = sharedQueue.interleaved; if (interleaved === null) { // This is the first update. Create a circular list. update.next = update; // At the end of the current render, this queue's interleaved updates will // be transfered to the pending queue. pushInterleavedQueue(sharedQueue); } else { update.next = interleaved.next; interleaved.next = update; } sharedQueue.interleaved = update; } else { const pending = sharedQueue.pending; if (pending === null) { // This is the first update. Create a circular list. update.next = update; } else { update.next = pending.next; pending.next = update; } sharedQueue.pending = update; } if (__DEV__) { if ( currentlyProcessingQueue === sharedQueue && ! didWarnUpdateInsideUpdate ) { console.error( 'An update (setState, replaceState, or forceUpdate) was scheduled ' + 'from inside an update function. Update functions should be pure, ' + 'with zero side-effects. Consider using componentDidUpdate or a ' + 'callback.', ); didWarnUpdateInsideUpdate = true; }}}Copy the code

If you look at this, you can see that this method adds the update of this update to the update queue, and the isBatchingUpdates attribute is not present in this version. It seems that React Fiber is still undergoing major changes, so I will write it here for now. If there are any new discoveries, I will add it here.

conclusion

  • Custom synthesized events and react hook functions update state asynchronously

  • State is updated synchronously in native events and setTimeout

Refer to the article

The React source