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

NestJs uses Mongoose to operate MongoDB

I recently started learning the NestJs framework....

Vue's detailed code for implementing the shuttle box function

Vue - implement the shuttle box function, the eff...

Meta tags in simple terms

The META tag, commonly referred to as the tag, is...

JavaScript using Ckeditor + Ckfinder file upload case detailed explanation

Table of contents 1. Preparation 2. Decompression...

Example of Vue routing listening to dynamically load the same page

Table of contents Scenario Analysis Development S...

Simple tutorial on using Navicat For MySQL

recommend: Navicat for MySQL 15 Registration and ...

MySQL merges multiple rows of data based on the group_concat() function

A very useful function group_concat(), the manual...

MySQL 8.0 user and role management principles and usage details

This article describes MySQL 8.0 user and role ma...

How to use boost.python to call c++ dynamic library in linux

Preface Recently I started using robot framework ...

Detailed steps to store emoji expressions in MySQL

Caused by: java.sql.SQLException: Incorrect strin...

Summary of a CSS code that makes the entire site gray

In order to express the deep condolences of peopl...

MySQL recursion problem

MySQL itself does not support recursive syntax, b...

Detailed steps for installing and configuring MySQL 5.7

1. Download MySQL 1. Log in to the official websi...

jQuery implements all selection and reverse selection operation case

This article shares the specific code of jQuery t...