Reference:

  1. React source code analysis series – Decrypt setState

  2. What happened after setState

You can’t setState multiple times

There are some interesting things about using setState in the React component’s componentDidMount event:

class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log(this.state.val); // log this.setState({val: this.state.val + 1}); console.log(this.state.val); // log setTimeout(() => {this.setState({val: this.state.val + 1}); console.log(this.state.val); // log this.setState({val: this.state.val + 1}); console.log(this.state.val); // the 4th time log}, 0); } render() { return null; }};Copy the code

Running this code, we can see that the screen prints 0, 0, 2, 3.

Why isn’t setState successful

This is not what we expected, so let’s look at the setState flow chart, and see what’s going on in this method

We can see that if we are in the batch update phase, all changed operations are stored in the pending queue. When we have completed the batch update collection phase, we read the operations in the pengding queue and process and update the state at once. So according to the above execution results, we can probably guess that the first two setState operations should be in the batch update phase, and these two operations are collected in the queue, that is, the state will not be changed temporarily in this phase, so the original value of 0 is still retained.

When setTiemout is performed, the current task queue is jumped out, so it is estimated that the batch update phase is also jumped out, so the current operation will be immediately reflected in the state (after the above changes, the state has changed to 1). So the next two operations will result in state values of 2 and 3. If that makes sense in terms of a task queue, the question is why is the componentDidMount event batch update?

React setState = setState

function enqueueUpdate(component) { // ... if (! batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); }Copy the code

That is, the isBatchingUpdates property of the batchingStrategy determines whether the batchingStrategy is currently in the batch update phase, and then the batchingStrategy performs the batch update.

So what is the batchingStrategy? It’s really just a simple object that defines a Boolean value for isBatchingUpdates and a batchedUpdates method. Here is a simplified definition code:

var batchingStrategy = { isBatchingUpdates: false, batchedUpdates: function(callback, a, b, c, d, e) { // ... batchingStrategy.isBatchingUpdates = true; transaction.perform(callback, null, a, b, c, d, e); }};Copy the code

Note that in the batchingStrategy batchedUpdates method, there is a transaction. Perform call. This brings us to the core concept of this article, Transaction.

Transaction

In the Transaction source code, there is a special ASCII diagram that visually explains what Transaction does.

/*
 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>
 */
Copy the code

As you can see, this is done internally by wrapping the method that needs to be executed in a Wrapper and hosting it to the Perform method provided by Transaction, which uniformly initializes and closes each Wrapper.

Decryption setState

So what does Transaction have to do with the different representations of setState? First, we simply categorize the four setStates. The first two belong to the same category because they are executed on the same call stack. The two setstates in setTimeout belong to another category, for the same reason. Let’s look at the setState call stack in componentDidMout:

The call stack of setState in setTimeout is as follows:

As you can see, the setState is executed wrapped in the Transaction of batchedUpdates. So who called the batchedUpdate method this time? Let’s go back one level to the _renderNewRootComponent method in reactmount.js. In other words, the React component is rendered into the DOM in one big Transaction.

This makes sense, because when setState is called in componentDidMount, isBatchingUpdates to batchingStrategy are already set to true, so the results of the two setStates don’t take effect immediately. It’s put in dirtyComponents. This also explains why this.state.val was printed with 0 twice, and the new state has not yet been applied to the component.

In setTimeout, the isBatchingUpdates flag bit of batchingStrategy is false, so the new setState will take effect immediately. I didn’t go to the dirtyComponents branch. That is, this.state.val becomes 1 when setState is first set in setTimeout, and this.state.val becomes 2 when setState is printed. The second setState is the same.

Why does setState fail multiple times on the click event

Let’s look at the following example

var Example = React.createClass({ getInitialState: function() { return { clicked: 0 }; }, handleClick: function() { this.setState({clicked: this.state.clicked + 1}); this.setState({clicked: this.state.clicked + 1}); console.log(this.state.clicked) }, render: function() { return <button onClick={this.handleClick}>{this.state.clicked}</button>; }});Copy the code

After executing, we can see that setState is called only once, and this.state.clicked equals 0

Detailed Process Description

Only part of the core process is left in the flowchart above, and it should be clear from this that all batchUpdate functionality is implemented by hosting it to Transaction. After this. SetState call, new state did not take effect immediately, but rather through ReactUpdates. BatchedUpdate method for temporary queue. When the outer transaction is completed, just call ReactUpdates. FlushBatchedUpdates method all the temporary state merge and calculate the latest props and the state.

Transaction can be used in many ways in the React source code, and the React source code comments list a number of ways in which Transaction can be used, such as

  • Does the selected text range in the input not change before and after a DOM reconciliation
  • Disable events when DOM nodes are rearranged to ensure that unnecessary blur/focus events are not triggered. At the same time, the event system can be restored to the enabled state after DOM retakes are completed.
  • When the worker thread’s DOM reconciliation is complete, the main thread updates the entire UI
  • Call all componentDidUpdate callbacks after rendering the new content, etc

React also exposes the batchUpdate method:

var batchedUpdates = require('react-dom').unstable_batchedUpdates;
Copy the code

When you need to call setState multiple times in non-DOM event callbacks, you can encapsulate your logic and call batchedUpdates to ensure that the Render method is not called multiple times.