Github address welcome star! Recently, my colleague encountered a problem of delayed rendering of react component, which was finally found to be caused by a poor understanding of state update. Specific problem description:

problems

// In the parent. Js component there is such a function and a Child of the Child. // Click the button to trigger the roleId change, and make an asynchronous request. -- parent-js --> this.state. Data from the data returned by ajax request <Child roleId={this.state.roleid} data={this.state. Data} /> <! -- Child.js--> class Child extends React.Component { constructor(props){ super(props); this.state = { data:null } }componentWillMount() {
        this.setState({
            data: this.props.data
        })
    }
    
    componentWillReceiveProps(nextProps) {
        if(nextProps.roleId ! ==this.props.roleId){ this.setState({ data: nextProps.data }) } } ...render() {
        return (
        <div>
        {
            this.state.data.map(val=>(<div>{val.name}</div>)) 
        }
        </div>
    }
    
}
Copy the code

The above code looks fine at first glance, but when the roleId is changed, the value in data is not changed, and when the roleId is changed, the data change is the result of the last time, there is a delay (asynchronous). You can use the React plugin in Chrome to see how values change in sequence.

Conclusion, because the data is produced after roleId change asynchronously, componentWillReceiveProps will not perform when initializing render, This is triggered when the Component receives a new Props (this function is triggered whenever the parent Component rerenders), meaning that the roleId changes without changing the data.

Use pure components in React to avoid props turning to state. Here is a brief analysis of the update of state.

Explore the state update policy in React

React state updates cannot be changed by assigning a value directly to it.

<! -- error --> this.state. Data = 1 // Never modify this.state directly. This is not only inefficient, but may be replaced by later operations. this.state.count + 1 }); This.setstate ({count: this.state.count + 1});Copy the code

Update via setState, and there are several ways to continuously update via setState:

  1. The setState function takes the second argument, which is the callback after state has been updated

setState(nextState, callback); // nextState (nextState, callback); // this.state.count + 1 }, () => { this.setState({ count: this.state.count + 1 }); });Copy the code
  1. The function way

NextState can also be a function, called a state-computing function, structured as function(state, props) => newState. This function enqueues each update, fetching the new state from the current state and props. So the example above could be written like this

this.setState((state, props) => {  
  return {count: state.count + 1};
});
this.setState((state, props) => {  
  return {count: state.count + 1};
});
Copy the code
  1. Use the React lifecycle function to place the logic that needs to be implemented after the setState update in an appropriate lifecycle hook function, such as componentDidMount or componentDidUpdate. “ComponentDidUpdate” = “componentDidUpdate” = “componentDidUpdate”;

State Updates May Be Asynchronous (Asynchronous)

The setState method and the execution involved in it is a very complex process, and React has undergone numerous changes since its inception. In addition to changing this.state, the React core diff algorithm is responsible for triggering a re-render, which ultimately determines whether to re-render and how to render. Moreover, for batch and performance reasons, multiple setState calls may need to be merged during execution, so it is quite reasonable that it is designed to be executed in a delayed manner.

SetState implements state updates through a queue mechanism. When setState is called, the state that needs to be updated will be merged and put into the status queue, instead of updating the state of this.state immediately (the queue mechanism can efficiently update the state in batches).

  1. When the value of this.state is directly modified, the state will not be placed in the state queue. The next call to setState and merge the state queue will ignore the state that was directly modified before, resulting in unpredictable errors.
  2. Avoid repeating state updates frequently

The specific source code is as follows:

<! --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.setState = function(partialState, callback) {
  ...
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
//setState calls a method of this.updater <! --ReactFiberClassComponent.js--> const classComponentUpdater = { ... enqueueSetState(inst, payload, callback) { const fiber = ReactInstanceMap.get(inst); const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime, fiber); const update = createUpdate(expirationTime); update.payload = payload;if(callback ! == undefined && callback ! == null) {if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update, expirationTime); // Join the update queue scheduleWork(Fiber, expirationTime); // Start scheduling updates},... }; <! --ReactFiberScheduler.js--> // requestWork is called by the scheduler whenever a root receives an update. // It's up to the renderer to call renderRoot at some point in the future. function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { addRootToSchedule(root, expirationTime); if (isRendering) { // Prevent reentrancy. Remaining work will be scheduled at the end of // the currently rendering batch. return; } if (isBatchingUpdates) { // Flush work at the end of the batch. if (isUnbatchingUpdates) { // ... unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpiration(expirationTime);
  }
}



<!-- ReactFiberScheduler.js-->
...
// TODO: Batching should be implemented at the renderer level, not inside
// the reconciler.
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return fn(a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}
...

<!--ReactDOMEventListener.js -->
...
export function dispatchEvent(
  topLevelType: DOMTopLevelEventType,
  nativeEvent: AnyNativeEvent,
) {
  if(! _enabled) {return;
  }

  const nativeEventTarget = getEventTarget(nativeEvent);
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);
  if( targetInst ! == null && typeof targetInst.tag ==='number' &&
    !isFiberMounted(targetInst)
  ) {
    // If we get an event (ex: img onload) before committing that
    // component's mount, ignore it for now (that is, treat it as if it was an // event on a non-React tree). We might also consider queueing events and // dispatching them after the mount. targetInst = null; } const bookKeeping = getTopLevelCallbackBookKeeping( topLevelType, nativeEvent, targetInst, ); try { // Event queue being processed in the same cycle allows // `preventDefault`. batchedUpdates(handleTopLevel, bookKeeping); } finally { releaseTopLevelCallbackBookKeeping(bookKeeping); }}...Copy the code

The actual code above is relatively complex and is represented by a pseudocode:

function interactiveUpdates(callback) {
    isBatchingUpdates = true; // Execute the event callback if there is a call tosetCallback (); callback(); callback(); isBatchingUpdates =false; performSyncWork(); // Start updating}Copy the code

Summary update Strategy

React setState determines whether to update this.state directly or put it in a queue later based on isBatchingUpdates. IsBatchingUpdates are false by default. SetState will synchronize this.state. However, there is a function called batchedUpdates that will set isBatchingUpdates to true. React calls batchedUpdates before calling the event handler.

SetState does not update this.state synchronously;

In cases outside of React control, setState updates this.state!

Out of control refers to the event handlers that bypass React and are added directly via addEventListener, as well as the asynchronous calls made via setTimeout/setInterval. For details, see JS Bin

Implement promisification about setState

The above shows that setState is an asynchronous update in general, so we can think of packaging it with promise:

function setStatePromise(that, newState) {
    return new Promise((resolve) => {
        that.setState(newState, () => {
            resolve();
        });
    });
}

Copy the code

The future of setState — functional setState

To quote Mo Cheng (author of React and Redux in Plain English), it’s great to have setState accept a function’s API design! This is because it is in line with the idea of functional programming, which allows developers to write functions without side effects. Instead of modifying component state, our functions simply return the “desired state change” to React, leaving React to do all the hard work of maintaining state.

function increment(state, props) {
  return{count: state.count + 1}; } // For multiple calls to the functionsetIn the case of State, React ensures that each time increment is called, State has merged the previous State changes.function incrementMultiple() { this.setState(increment); this.setState(increment); this.setState(increment); } / / join the current this. State. The count value is 0, the first call to enclosing setState (increment), to the increment of the state parameter is 0, the second call, the state parameter is 1, the call is the third time, and parameter is 2, In the end, incrementMultiple really does have the effect of making this.state.count 3, which is what incrementMultiple deserves. // When increment is called, this.state is not changed until the render function is re-executed (or shouldComponentUpdate returns)falseLater) was changedCopy the code

For the setState function above, you might think of mixed cases:

function incrementMultiple() { this.setState(increment); this.setState(increment); this.setState({count: this.state.count + 1}); this.setState(increment); } // The result is to increase this.state.count by 2 instead of 4.Copy the code

The reason: React consolidates all setState effects in sequence. Although the first two function setState calls produce count plus 2, a conventional setState call forces the accumulated effects to be emptied. I’m going to replace it with count plus 1.

With that said, the key point about setState is summarized:

  1. SetState does not immediately change the value of state in the React component;
  2. SetState causes a redraw by causing a component update process;
  3. The effects of multiple setState calls are merged.
However, the blog did not sort out some important concepts in the source code, and some key points of interpretation, react design philosophy and so on, please allow to share next time

If there is any mistake or not precise place, please be sure to give correction, thank you very much!

Reference:

  1. www.zhihu.com/question/66…
  2. Juejin. Cn/post / 684490…
  3. Juejin. Cn/post / 684490…
  4. Reactjs.org/docs/state-…
  5. Deep in the React Tech Stack
  6. An exploration of setState promisification in React
  7. SetState When to update the status