Do you really know setState()?

Detailed Interpretation of setState() in React

For setState(), which is officially recommended by React as an API for updating component state. But do you really know about setState()? Let me talk about it slowly.

setState() Official Usage Guide

Syntax 1: setState(updater[, callback])

  • updater: Function type that returns a status object in the updated state, which merges shallowly with the state.

  • Callback: Optional, callback function.

Syntax 2: setState(stateChange[, callback])

  • setState: Object type that merges shallow incoming objects into a new state.

  • Callback: Optional, callback function.

For these two forms, the difference is the first parameter selection problem. You can choose a function to return a new state object, or you can directly select an object to apply to the state update. When do you choose the parameters of the function type and when do you choose the object type? Here we can sum up two sentences:

  • When the current state update does not depend on the previous state, select the object type parameter

  • When the current update state depends on the previous state, select the function type parameter

example:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>setState Detailed explanation</title>
  <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel">
    class A extends React.Component {
      state = {
        count: 0
      }
      update1 = () => {
        this.setState({count: this.state.count+1})
      }

      update2 = () => {
        this.setState(state => ({
          count: state.count+1
        }))
      }

      update3 = () => {
        this.setState({
          count: 8
        })
      }  

      render () {
        return (
          <div>
            <h1>{this.state.count}</h1>
            <button onClick={this.update1} style={{marginRight: 15}}>Test 1</button><button style={{marginRight: 15}} onClick={this.update2}>Test 2</button><button onClick={this.update3}>Test 3</button>
          </div>         
        )
      }
    }
    ReactDOM.render(
    <A/>,
    document.getElementById('app')
    )
  </script>
</body>
</html>

In this example, we change the count state value of component A by clicking on the button test 1 or test 2, because each change state is added 1 on the original basis, so it is suitable to select function type parameters in setState, i.e. update2 writing recommendation.

Clicking the test 3 button will directly change the count value to a fixed value of 8, which does not depend on the last count state value, so it is suitable to select the object type parameter in setState, i.e. update3 Writing Recommendation.

Is the setState() update state necessarily asynchronous?

We know that setState() triggers the render() function of the component. Rendering the component displays the updated content on the view. Can we get the latest state value immediately after setState()?

Is setState() updated asynchronously or synchronously?

Conclusion:

  • In the React-related callback function, setState() is an asynchronous update

  • setState() is not updated synchronously in React-related callbacks

React-related callbacks include: component lifecycle hooks, React component event listening callbacks.

React unrelated callbacks include common setTimeout(), Promise(), and so on.

We can still take the previous button click example to test.

example:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>setState Detailed explanation</title>
  <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel">
    class A extends React.Component {
      state = {
        count: 0
      }
      update1 = () => {
        this.setState({count: this.state.count+1})
        console.log(this.state.count)
      }

      update2 = () => {
        setTimeout(() => {
          this.setState(state => ({
            count: state.count+1
          }))
          console.log(this.state.count)
        })
      }

      update3 = () => {
        Promise.resolve().then(value => {
          this.setState({
            count: 8
          })
          console.log(this.state.count)
        })
      }

      componentWillMount () {
        this.setState(state => ({
          count: state.count+1
        }))
        console.log(this.state.count)
      }

      render () {
        console.log('render()', this.state.count)
        return (
          <div>
          <h1>{this.state.count}</h1>
            <button onClick={this.update1} style={{marginRight: 15}}>Test 1</button><button style={{marginRight: 15}} onClick={this.update2}>Test 2</button><button onClick={this.update3}>Test 3</button>
          </div>         
        )
      }
    }
    ReactDOM.render(
    <A/>,
    document.getElementById('app')
  )
  </script>
</body>
</html>

We print the latest state value after setState() in the React event listener callback update1 and component lifecycle component WillMount () hook respectively. We find that the printed state is the state before the modification, but the page has been updated to the latest state. See the picture:

In the same way, we can observe that in the setTimeout() callbacks of update2 and Promise() callbacks of update3, the latest state value is printed after setState(), and this printing will occur after setState() triggers the component to render(). After testing, it is proved that our conclusion is correct. setState() is an asynchronous update state in React-related callbacks, and setState() is a synchronous update state in unrelated callbacks.

When setState() asynchronously updates the state, how do you get the latest state value?

This question is really about how to get the latest state value immediately after setState() asynchronously updates the state, that is, how to print the latest state value in update1() and componentWillMount().

The answer is actually very simple, that is, the second callback() parameter of the setState() parameter we talked about. The second callback of setState() is invoked after the state is updated and the component is render(), where we can get the latest state value.

Code:

...

update1 = () => {
  this.setState({count: this.state.count+1}, () => {
    console.log(this.state.count)
  })
}

componentWillMount () {
  this.setState(state => ({
    count: state.count+1
  }), () => {
      console.log(this.state.count)
  })
}

In this way, we can also print out the latest state values in update1 and component WillMount ().

How does React handle repeated calls to setState()?

The premise we are discussing here is, of course, when setState() asynchronously updates the state, because when we call setState() several times for synchronous updates, we trigger render hooks several times, and of course print out the updated state values separately in real time.

Conclusion:

There are two situations to discuss here:

  • When setState() passes an object type parameter, React combines repeated calls to setState() to trigger a render.

  • When setState() passes function type parameters, React calls setState() several times in turn, triggering a render.

As you can see, we repeatedly call setState(), regardless of the type of reference. React calls render only once to re-render the component.

We can also test our conclusions by clicking on an example of a button.

example:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>setState Detailed explanation</title>
  <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel">
    class A extends React.Component {
      state = {
        count: 0
      }
      update1 = () => {
        // this.setState({count: this.state.count+1}, () => {
        //   console.log(this.state.count)
        // })
        // this.setState({count: this.state.count+1}, () => {
        //   console.log(this.state.count)
        // })
        // this.setState({count: this.state.count+1}, () => {
        //   console.log(this.state.count)
        // })
        this.setState((state) => ({
          count: state.count+1
        }), () => {
          console.log(this.state.count)
        })
        this.setState((state) => ({
          count: state.count+1
        }), () => {
          console.log(this.state.count)
        })
        this.setState((state) => ({
          count: state.count+1
        }), () => {
          console.log(this.state.count)
        })
      }

      update2 = () => {
        setTimeout(() => {
          this.setState(state => ({
            count: state.count+1
          }))
          console.log(this.state.count)
          this.setState(state => ({
            count: state.count+1
          }))
          console.log(this.state.count)
          this.setState(state => ({
            count: state.count+1
          }))
          console.log(this.state.count)
        })
      }

      update3 = () => {
        Promise.resolve().then(value => {
          this.setState({
            count: 8
          })
          console.log(this.state.count)
        })
      }

      componentWillMount () {
        this.setState(state => ({
          count: state.count+1
        }))
        console.log(this.state.count)
      }

      render () {
        console.log('render()', this.state.count)
        return (
          <div>
          <h1>{this.state.count}</h1>
            <button onClick={this.update1} style={{marginRight: 15}}>Test 1</button><button style={{marginRight: 15}} onClick={this.update2}>Test 2</button><button onClick={this.update3}>Test 3</button>
          </div>         
        )
      }
    }
    ReactDOM.render(
    <A/>,
    document.getElementById('app')
  )
  </script>
</body>
</html>

When you click the test button 2, because setState() is a synchronous update state, you can find that the component made several render calls and printed out the updated state value in turn. This is very simple.

We tested the parameters passed to setState() by clicking Test Button 1, and found that when the parameter is the object type, React will merge and repeat setState() calls, that is, only update the state once, and calculate and update the parameters of the function type separately.

Regardless of which way setState() is called repeatedly, React will only make one render call, which is also part of performance optimization to prevent performance problems caused by repeated rendering.

In fact, the official website recommends that when we use setState(), the first parameter passes the function type parameter, because the state and props received in the function parameter are guaranteed to be up-to-date.

Tags: Javascript React IE

Posted on Sun, 25 Aug 2019 05:46:53 -0700 by ElArZ