React documentation has three points to note when describing setState:

  1. Do not modify State directly
  2. Updates to State can be asynchronous
  3. State updates are merged

Let’s analyze each of these three limitations.

Do not modify state directly

The official documentation does not specifically explain why state cannot be changed directly. Only the constructor can assign state directly. If we change state directly, the component will not trigger the update.

I understand these points for this design:

  1. Updating the state means re-rendering the component, which can be done with a unified function to make the process more manageable; React doesn’t use two-way data binding like Vue does, so it needs a way to collect state changes.
  2. React’s core idea is immutable data. SetState actually returns a new state inside. If you change the state directly, you may still have the same reference.
  3. Updating state by a unified function is more convenient for maintenance and control.
  4. React may merge multiple setState operations into one for performance reasons.

React is designed with View=F(state). React renders views by maintaining an immutable state. When updating a view, you need to compare the old state with the new state to know the minimum changes and then perform updates.

There are also some performance optimizations involved, so providing a function that changes state permanently makes the entire render/update process more manageable.

Updates to State can be asynchronous

React may combine multiple calls into a single call for performance reasons. So we should not expect to understand the updated state after performing setState.

So exactly when is setState synchronous and when is it asynchronous?

Conclusion: setState updates performed via the React lifecycle function and synthesized events are asynchronous. SetState is synchronized through events such as setTimeout timers and native JS bindings such as dom.addeventListener.

If we look at the setState source:

ReactComponent.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); }}; enqueueSetState: function(publicInstance, partialState) { var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState', ); var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); }, function enqueueUpdate(component) { ensureInjected(); // It is very important to determine whether the update is synchronous or asynchronous. batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; }}Copy the code

BatchingStrategy isBatchingUpdates determines that this is a setState calls synchronous update state or asynchronous update.

BatchingStrategy isBatchingUpdates default is false. This value change is controlled by the batchedUpdates function. BatchedUpdates will change isBatchingUpdates to true. Then setState would be updated asynchronously.

So if we know when batchedUpdates are called, we also know when setState is executed synchronously and when it’s executed asynchronously.

There is a concept of transactions that needs to be mentioned. When a transaction starts, batchedUpdates are called to set isBatchingUpdates to true, and when the transaction ends, isBatchingUpdates are set to false.

React synthetic events and lifecycle functions generate a transaction when executed, so the setState executed in these cases is asynchronous.

However, if we use something like setTimeout to execute the task in the next event loop, at this time, because the transaction has already been executed, the execution of setState will behave as synchronization.

Through the document. The addEventListener execution of events is the same principle.

Representation of state in hooks

/ / scenario 1
const [count, setCount] = useState(1);

useEffect(() = > {
  setCount(2);
  console.log('count', count); // 1, async} []);/ / scenario 2
const [count, setCount] = useState(1);

useEffect(() = > {
  setTimeout(() = > {
    setCount(2);
    console.log("count", count); // 1, async
  }, 10); } []);/ / scenario 3
const [count, setCount] = useState(1);

const onClick = () = > {
  setCount(2);
  console.log("count", count); // 1, async
};

return <div onClick={onClick}>{count}</div>;

/ / scenario 4
const [count, setCount] = useState(1);

useEffect(() = > {
  document.getElementById("demo").addEventListener("click".() = > {
    setCount(2);
    console.log("count", count); // 1, async
  });
});

return <div id="demo">{count}</div>;
Copy the code

From the above four scenarios, we can see that setState implemented by hooks, either setTimeout or native method addEventListener, bypasses the React transaction but still does not get the latest values immediately. The reason for this is that we need to know what the functional component’s render actually does each time.

SetTimeout is executed asynchronously, but because of the closure, it still gets the same count as the first time, so it behaves differently than a class component.

State updates are merged

This is easier to understand because a change in state means that the component needs to render again. React collects the change in state to optimize performance, and only executes render once for multiple calls.

But if you do setState multiple times in setTimeout as described above, it will result in multiple render, so avoid this in your daily development.