Detailed explanation of react setState

Detailed explanation of react setState

Is setState synchronous or asynchronous?

Asynchronously updating state in custom synthetic events and react hook functions

Take setState in a custom click event as an example

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;

Click once, and the final printed result of this.state.count is 1, and the page displays 2. From the phenomenon, it can be seen that only the last setState of the three setStates is effective, and the first two setStates have no effect. Because if the first setState is changed to +3, the count print result is 1 and the display result is 2, and there is no change. And there is no synchronization to obtain the count result.

At this point, we can adjust the code to get the updated state through the second parameter of 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;

At this time, click once, and the print results of the three setState callback functions are as follows.

1
1: 2
twenty two
3: 2

First, the last line prints 1 directly. Then, in the callback of setState, the printed results are all the latest updated 2. Although the first two setState calls did not take effect, 2 will still be printed in their second parameter.

At this time, 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;

At this time, the printed result is 1, but the count displayed on the page is 4. It can be found that if setState updates the state by passing parameters, several setState updates will not only update the last one, but will take effect for several updates.

Next, let's see how much 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;

At this time, click once, and the print results in the three setState callback functions are as follows. As you can imagine, the page display result is also 4

1
1: 4
twenty four
3: 4

Put the above code into componentDidMount, and the output result will be the same as above.

Because, we can know that in custom synthetic events and hook functions, the state update is asynchronous.

Synchronously update state in native events and setTimeout

Take setState in setTimeout as an example

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;

At this time, the printed results are as follows:

1: 2
twenty three
3: 4
4

Replace the first parameter of 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);
}

The printed results are the same as above.

Do you feel that the state is fully controllable? In setTimeout, multiple setState calls will take effect, and the updated state can be obtained in the second parameter of each setState.

Similarly, the output results in the native event are consistent with those in setTimeout and are also synchronized.

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;

setState related source code

The following codes are all from react17.0.2 version

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');
};

setState can receive two parameters. The first parameter can be object, function, null, or undefined, and no error will be thrown. Execute the this.updater.enqueueSetState method below. Search enqueueSetState globally and find this variable in two directories.

First, the first set of directories:

Directory ./packages/react/src/ReactNoopUpdateQueue.js Line 100 enqueueSetState method, the parameters are this, initialized state, callback, and string setState, this refers to the current React instance.

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

Next, look at 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;
  }
}

This code is equivalent to adding an attribute to the didWarnStateUpdateForUnmountedComponent object. The key of the attribute is the React component that is currently setState.setState. If this attribute exists, it will be returned; if this attribute does not exist or the value of this attribute is false, the value of this attribute will be set to true.

Let’s look at another directory:

Directory ./react-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);
    }
  }
}

The main function is enqueueUpdate.

Directory ./react-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 transferred 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;
    }
  }
}

Seeing this, we find that this method adds the update of this update to the update queue, but the isBatchingUpdates property is not found in this version. It seems that the changes to React Fiber are quite large, so I will stop here for now and add more if I find anything new.

Summarize

  • Asynchronously updating state in custom synthetic events and react hook functions
  • Synchronously update state in native events and setTimeout

The above is a detailed explanation of react setState. For more information about react setState, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • Deep understanding of React State principles
  • A brief analysis of React's understanding of state
  • Detailed explanation of the abbreviation of state in react
  • In-depth study of setState source code in React
  • In-depth understanding of the working mechanism of react's setState
  • How much do you know about state and setState() in React components?

<<:  Example analysis to fix problems in historical Linux images

>>:  A detailed discussion of MySQL deadlock and logs

Recommend

Use semantic tags to write your HTML compatible with IE6,7,8

HTML5 adds more semantic tags, such as header, fo...

Solution to SNMP4J server connection timeout problem

Our network management center serves as the manag...

img usemap attribute China map link

HTML img tag: defines an image to be introduced in...

How to install redis5.0.3 in docker

1. Pull the official 5.0.3 image [root@localhost ...

Podman boots up the container automatically and compares it with Docker

Table of contents 1. Introduction to podman 2. Ad...

Detailed tutorial on deploying Springboot or Nginx using Kubernetes

1 Introduction After "Maven deploys Springbo...

How to redirect to https through nginx load balancing

Copy the certificate and key on the web scp -rp -...

How to install Graphviz and get started tutorial under Windows

Download and installConfigure environment variabl...

A brief discussion on the placement of script in HTML

I used to think that script could be placed anywh...

Linux kernel device driver kernel debugging technical notes collation

/****************** * Kernel debugging technology...