About the setState

Whether setState update is synchronous or asynchronous has always been a hot topic. However, if we actually need to use the updated status value, we don’t have to rely heavily on the synchronous/asynchronous update mechanism. In the class component, we can use the second parameter of this.setState, componentDidMount, componentDidUpdate and other means to obtain the updated value; In functional components, you can useEffect to get the updated state. So this question is actually kind of boring.

However, since everyone is so willing to discuss, today we will systematically comb through this problem, mainly divided into two aspects:

  • Class components (class-component) update mechanism
  • Functional components (function-component) update mechanism

Class component this.setState

In class components, the answer to this question varies, starting with the first conclusion:

  • inlegacyIn the pattern, updates can be synchronous or asynchronous;
  • inconcurrentIn the mode, it must be asynchronous.

What are Legacy mode and Concurrent mode?

  • ReactDOM. Render (
    , rootNode) is used to create the App in Legacy mode, which is currently the default mode used by create-React-app.

  • Applications created by reactdom.unstable_createroot (rootNode).render(
    ) are in concurrent mode, which is currently an experimental product and not yet mature.

Can it be synchronous or asynchronous in Legacy mode?

Yes, this is not metaphysics. Let’s throw out the conclusion and explain it step by step.

  1. When called directlythis.setStateIs asynchronous update.
  2. When called in the callback of an asynchronous functionthis.setState, is synchronous update.
  3. When placed in a custom DOM event handler, it is also updated synchronously.

The experimental code is as follows:

class StateDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            count: 0}}render() {
        return <div>
            <p>{this.state.count}</p>
            <button onClick={this.increase}>cumulative</button>
        </div>
    }
    increase = () = > {
        this.setState({
            count: this.state.count + 1
        })
        // Async, can not get the latest value
        console.log('count'.this.state.count)

        // setState is synchronized in setTimeout
        setTimeout(() = > {
            this.setState({
                count: this.state.count + 1
            })
            // You can get it
            console.log('count in setTimeout'.this.state.count)
        }, 0)
    }

    bodyClickHandler = () = > {
        this.setState({
            count: this.state.count + 1
        })
        // Get the latest value
        console.log('count in body event'.this.state.count)
    }

    componentDidMount() {
        // The setState of the DOM event is synchronized
        document.body.addEventListener('click'.this.bodyClickHandler)
    }
    componentWillUnmount() {
        // Destroy custom DOM events in time
        document.body.removeEventListener('click'.this.bodyClickHandler)
    }
}
Copy the code

To answer the above phenomenon, you must understand the main flow of setState and the batchUpdate mechanism in React.

First, let’s look at the main flow of setState:

  1. callthis.setState(newState);
  2. newStateStores to the pending queue;
  3. Judge whether or notbatchUpdate;
  4. If it isbatchUpdate, the component is first saved in the so-called dirty componentdirtyComponents; If it is notbatchUpdate, then go through all the dirty components and update them.

BatchUpdate = batchUpdate = batchUpdate = batchUpdate; Synchronous updates, on the other hand, always go through all the dirty components and update them.

Very interesting, it seems that hitting batchUpdate is the key. The question then arises as to why batchUpdate can be hit by a direct call, but not by an asynchronous callback or custom DOM event.

This brings up an interesting point: react’s implementation mechanism for registration entries. For the increase function we just registered, there are still some things we can’t see when executing, which we now magically make manifest:

increase = () = > {
        // Start: bashUpdate by default
        // isBatchingUpdates = true
        this.setState({
            count: this.state.count + 1
        })
        console.log('count'.this.state.count)
        / / end
        // isBatchingUpdates = false

    }
Copy the code
    increase = () = > {
        // Start: bashUpdate by default
        // isBatchingUpdates = true
        setTimeout(() = > {
            // At this point isBatchingUpdates are set to false
            this.setState({
                count: this.state.count + 1
            })
            console.log('count in setTimeout'.this.state.count)
        }, 0)
        / / end
        // isBatchingUpdates = false
    }
Copy the code

When react executes the function we wrote, the isBatchingUpdates variable is set at the start and end by default. See the difference? When setTimeout executes its callback, isBatchingUpdates will already be set to false at the end of the synchronization code, so batchUpdate will not be added.

What about custom DOM events? The code is still as follows:

  componentDidMount() {
    // Start: bashUpdate by default
    // isBatchingUpdates = true
    document.body.addEventListener("click".() = > {
      // In the callback function, isBatchingUpdates are already set to false when the click event is triggered
      this.setState({
        count: this.state.count + 1});console.log("count in body event".this.state.count); // Get the latest value.
    });
    / / end
    // isBatchingUpdates = false
  }
Copy the code

We can see that isBatchingUpdates are already set to false when componentDidMount runs out, and isBatchingUpdates are also set to false when the click event is triggered later and the callback is called. The batchUpdate mechanism will not be hit.

Conclusion:

  • this.setStateSynchronous or asynchronous, the key is to see if you can hitbatchUpdatemechanism
  • Can hit, is to seeisBatchingUpdatesistrueorfalse
  • Can hitbatchUpdateScenarios include life cycles and their calling functions, events registered in React and their calling functions. React is the gateway through which React can manage.

React adds isBatchingUpdate not for functions, but for entrances. SetTimeout, setInterval, custom DOM event callbacks, etc. These are the React “do not manage” entry, so do not set the isBatchingUpdates variable at the beginning and end.

Concurrent mode must be an asynchronous update

To enable concurrent mode, update React to the experimental version as well. Install the following dependencies:

npm install react@experimental react-dom@experimental
Copy the code

The rest of the code remains unchanged, except to change the index file as follows:

- ReactDOM.render(<App />, document.getElementById('root'));

+ ReactDOM.unstable_createRoot(document.getElementById('root')).render(<App />);
Copy the code

You can see that its updates are asynchronous, in any case.

Setters for useState in functional components

In functional components, we define state like this:

const [count, setCount] = useState(0)
Copy the code

When we call setCount either in a synchronous function or in an asynchronous callback, the printed count is the old value, so we say setCount is asynchronous.

  const [count, setCount] = useState(0);

  // Call directly
  const handleStrightUpdate = () = > {
    setCount(1);
    console.log(count); / / 0
  };

  // in the setTimeout callback
  const handleSetTimeoutUpdate = () = > {
    setTimeout(() = > {
      setCount(1);
      console.log(count); / / 0
    });
  };
Copy the code

SetCount is asynchronous, which is true, but there is more to it than just asynchronous updates. There are two main reasons for this:

When setCount is called React merges setsetters to update the hooks list asynchronously for the function component and triggers re-renders. For example, setCount is an asynchronous operation. For example, when setCount is called React consolidates setsetters to update the hooks list asynchronously for the function component and triggers re-renders.

2. The function capture-value determines that the console.log(count) statement always prints a constant that exists only in the current frame, so regardless of whether setCount is synchronous or asynchronous, the old value is actually always printed.