SetState is the most frequently used API in React (before hooks, of course). It has a wide variety of uses and is also a common topic in React interview questions.

In this article, I have done a lot of analysis of React setState, hoping to help you understand setState. (There is a source code involved in React. I have posted it, but I didn’t expand it in detail. We will analyze the source code if we have a chance.

The use of setState

1.1. Why use setState

Going back to the original example, when clicking on a button to change text, modify what is displayed on the screen:

The basic code for the case is as follows:

import React, { Component } from 'react'

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "Hello World"}}render() {
    return (
      <div>
        <h2>{this.state.message}</h2>
        <button onClick={e= >This.changetext ()}> Change the text</button>
      </div>)}changeText(){}}Copy the code

The key is how it should be implemented in changeText:

Can we modify the interface by directly modifying message in State?

  • Clicking doesn’t do anything. Why?
  • After modifying the state, we expected React to re-render the interface according to the latest state. However, we did not know that the data had changed in this way.
  • React does not implement a way to listen for data changes, like object. defineProperty in Vue2 or Proxy in Vue3.
  • We must use setState to tell React that the data has changed;
  changeText() {
    this.state.message = "Hello, Li Yinhe.";
  }
Copy the code

We must update the data with setState:

  • Confused: there is no setState method implemented in the component, why can call it?
  • The reason is simple: the setState method is inherited from Component.
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');
};
Copy the code

So, we can modify the data by calling setState:

  • When we call setState, the render function is re-executed to create the ReactElement object based on the latest State;
  • DOM is modified according to the latest ReactElement object.
changeText() {
  this.setState({
    message: "Hello, Li Yinhe."})}Copy the code

1.2. setState asynchronous update

Let’s look at the following code:

  • The final print is Hello World;
  • It can be seen that setState is an asynchronous operation, and we cannot get the result of the latest state immediately after executing setState
changeText() {
  this.setState({
    message: "Hello, Li Yinhe."
  })
  console.log(this.state.message); // Hello World
}
Copy the code

Why is setState designed to be asynchronous?

  • There was a lot of discussion on GitHub about setState being asynchronous;
  • React core member (Redux’s author) Dan Abramov also has a response for those who are interested.
  • Github.com/facebook/re…

Let me briefly summarize his answer:

  • setStateDesigned as asynchronous, it can significantly improve performance;
    • If setState is updated every time, it means that the render function will be called frequently and the interface will be re-rendered, which is inefficient;
    • The best thing to do is to get multiple updates and then batch them;
  • If the synchronization updates state, but the render function has not been executed, then state and props cannot be kept in sync;
    • The inconsistency between the props and the state can cause a lot of problems in development.

So how can I get the updated value?

  • SetState takes two arguments: the second argument is a callback function that will be executed after the update;
  • The format is as follows:setState(partialState, callback)
changeText() {
  this.setState({
    message: "Hello, Li Yinhe."
  }, () = > {
    console.log(this.state.message); // Hello, Li Yinhe
  });
}
Copy the code

Of course, we can also use life cycle functions:

componentDidUpdate(prevProps, provState, snapshot) {
  console.log(this.state.message);
}
Copy the code

1.3. SetState must be asynchronous?

Confused: setState must be asynchronously updated?

Validation 1: Updates in setTimeout:

changeText() {
  setTimeout(() = > {
    this.setState({
      message: "Hello, Li Yinhe."
    });
    console.log(this.state.message); // Hello, Li Yinhe
  }, 0);
}
Copy the code

Validation two: native DOM events:

componentDidMount() {
  const btnEl = document.getElementById("btn");
  btnEl.addEventListener('click'.() = > {
    this.setState({
      message: "Hello, Li Yinhe."
    });
    console.log(this.state.message); // Hello, Li Yinhe})}Copy the code

There are actually two cases:

  • In the component life cycle or React composite event, setState is asynchronous;
  • In setTimeout or native DOM events, setState is synchronous;

The React of is determined by a function to: enqueueSetState partial implementation (React – the reconciler/ReactFiberClassComponent. Js)

enqueueSetState(inst, payload, callback) {
  const fiber = getInstance(inst);
  // The React context computes a current time
  const currentTime = requestCurrentTimeForUpdate();
  const suspenseConfig = requestCurrentSuspenseConfig();
  // This function returns whether the update is currently synchronous or asynchronous (priority, to be exact)
  const expirationTime = computeExpirationForFiber(
    currentTime,
    fiber,
    suspenseConfig,
  );

  constupdate = createUpdate(expirationTime, suspenseConfig); . }Copy the code

ComputeExpirationForFiber function of a part of the implementation:

  • Sync is the highest priority, i.e., update-on-creation;
  currentTime: ExpirationTime,
  fiber: Fiber,
  suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
  const mode = fiber.mode;
  if ((mode & BlockingMode) === NoMode) {
    return Sync;
  }

  const priorityLevel = getCurrentPriorityLevel();
  if ((mode & ConcurrentMode) === NoMode) {
    return priorityLevel === ImmediatePriority ? Sync : Batched;
  }
Copy the code

1.4. Consolidation of setState

1.4.1. Data consolidation

Let’s say we have data like this:

this.state = {
  name: "coderwhy".message: "Hello World"
}
Copy the code

We need to update message:

  • If I modify message through setState, it will not affect name.
changeText() {
  this.setState({
    message: "Hello, Li Yinhe."
  });
}
Copy the code

Why wouldn’t it matter? In fact, there is a merge of the original object and the new object:

  • In fact, usingObject.assign(target, ... sources)2. To accomplish;

1.4.2. Multiple SETStates merged

For example, we still have a counter property that keeps track of the current number:

  • What would counter be if I did the following? The answer is 1;
  • Why is that? Because it merges multiple states;
  increment() {
    this.setState({
      counter: this.state.counter + 1
    });

    this.setState({
      counter: this.state.counter + 1
    });

    this.setState({
      counter: this.state.counter + 1
    });
  }
Copy the code

ProcessUpdateQueue has a do… While loop, which takes states from the queue and merges them;

How do I make it so that counter ends up being 3?

increment() {
  this.setState((state, props) = > {
    return {
      counter: state.counter + 1}})this.setState((state, props) = > {
    return {
      counter: state.counter + 1}})this.setState((state, props) = > {
    return {
      counter: state.counter + 1}})}Copy the code

Why does passing in a function make 3?

  • The reason is that when multiple states are merged, the function is executed once for each traversal:

SetState performance optimization

React Update mechanism

We learned how to render React earlier:

What about the React update process?

React calls the Render method of React when the props or state changes, creating a different tree.

React needs to determine how to effectively update the UI based on the difference between the two different trees:

  • If a tree is completely updated by referring to another tree, then even the most advanced algorithm has a complexity of O(n 3), where n is the number of elements in the tree;
  • Grfia. Dlsi. Ua. Es/ml/algorith…
  • If this algorithm was used in React, the amount of computation required to display 1000 elements would be on the order of a billion;
  • This overhead was too expensive, and React’s update performance became inefficient;

React optimizes this algorithm to O(n). How?

  • The comparison between nodes of the same layer will not break the node comparison;
  • Different types of nodes produce different tree structures.
  • In development, keys can be used to specify which nodes remain stable under different renders;

2.2. Diffing algorithm

2.2.1. Compare different types of elements

When nodes have different elements, React disassembles the existing tree and creates a new one:

  • When an element from<a>become<img>, from<Article>become<Comment>, or from<Button>become<div>Triggers a complete rebuild process;
  • When a tree is unmounted, the corresponding DOM node is also destroyed and the component instance executescomponentWillUnmount()Methods;
  • When a new tree is created, the corresponding DOM node is created and inserted into the DOM, and the component instance is executedcomponentWillMount()Method, and thencomponentDidMount()Methods;

Such as the following code change:

  • The React will destroyCounterComponent and reload a new component without reusing Counter;
<div>
  <Counter />
</div>

<span>
  <Counter />
</span>
Copy the code

2.2.2. Compare elements of the same type

When you compare two React elements of the same type, React preserves the DOM node and only compares and updates the changed attributes.

Such as the following code change:

  • By comparing the two elements, React knows that it only needs to modify theclassNameProperties;
<div className="before" title="stuff" />

<div className="after" title="stuff" />
Copy the code

Such as the following code change:

  • When the updatestyleReact only updates properties that have changed.
  • By comparing the two elements, React knows that it only needs to modify thecolorStyle, no modification requiredfontWeight.
<div style={{color: 'red'.fontWeight: 'bold'}} / ><div style={{color: 'green', fontWeight: 'bold'}} / >
Copy the code

If it is a component element of the same type:

  • The component stays the same, React updates the props of the component, and callscomponentWillReceiveProps()componentWillUpdate()Methods;
  • Next, callrender()Method, the diff algorithm will recurse between the previous result and the new result;

2.2.3. Recurse the child nodes

By default, React iterates through lists of both child elements while recursively iterating through the child elements of a DOM node. When a difference occurs, a mutation is generated.

Let’s look at inserting a data item at the end:

  • The first two comparisons are identical, so no mutation will occur;
  • In the final comparison, a mutation is generated and inserted into the new DOM tree.
<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>
Copy the code

But if we insert a piece of data in the middle:

  • React produces a mutation on each child element instead of holding it<li> Interstellar </li>and<li> </li>The same;
  • This inefficient way of comparing results in performance problems;
<ul>
  <li>Interstellar through</li>
  <li>inception</li>
</ul>

<ul>
  <li>A Chinese Odyssey</li>
  <li>Interstellar through</li>
  <li>inception</li>
</ul>
Copy the code

2.3. Optimization of keys

As we walked through the list earlier, we were always prompted with a warning to add a key property:

Let’s take a look at an example:

import React, { Component } from 'react'

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      movies: ["Interstellar".Inception]}}render() {
    return (
      <div>
        <h2>The movie list</h2>
        <ul>
          {
            this.state.movies.map((item, index) => {
              return <li>{item}</li>})}</ul>
        <button onClick={e= >This.insertmovie ()}> Inserts data</button>
      </div>)}insertMovie(){}}Copy the code

Method 1: Insert data in the last position

  • In this case, it doesn’t really matter
insertMovie() {
  const newMovies = [...this.state.movies, "A Chinese Odyssey"];
  this.setState({
    movies: newMovies
  })
}
Copy the code

Method 2: Insert data in front

  • In this way, in the case of no key, all li needs to be modified;
insertMovie() {
  const newMovies = ["A Chinese Odyssey". this.state.movies];this.setState({
    movies: newMovies
  })
}
Copy the code

When a child element (in this case li) has a key, React uses the key to match the child element on the original tree with the child element on the latest tree:

  • In the following scenario, elements with keys 111 and 222 are simply shifted without any modification;
  • Insert the element with key 333 at the top;
<ul>
  <li key="111">Interstellar through</li>
  <li key="222">inception</li>
</ul>

<ul>
  <li key="333">Connecticut</li>
  <li key="111">Interstellar through</li>
  <li key="222">inception</li>
</ul>
Copy the code

Key notes:

  • Key should be unique;
  • Do not use a random number for key (a random number will be regenerated by the next render);
  • Using index as key is not optimized for performance;

2.4. Optimization of SCU

2.4.1. The render function is called

Let’s use an earlier nested example:

  • In the App, we added a counter code;
  • When +1 is clicked, the App’s render function is called again.
  • When the render function of the App is called, the render function of all subcomponents will be called again.
import React, { Component } from 'react';

function Header() {
  console.log("Header Render is called");
  return <h2>Header</h2>
}

class Main extends Component {
  render() {
    console.log("Main Render is called");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>)}}function Banner() {
  console.log("Banner Render called");
  return <div>Banner</div>
}

function ProductList() {
  console.log("ProductList Render called");
  return (
    <ul>
      <li>Item 1</li>
      <li>2 goods</li>
      <li>Product 3</li>
      <li>Goods 4</li>
      <li>Goods 5</li>
    </ul>)}function Footer() {
  console.log("Footer Render is called");
  return <h2>Footer</h2>
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0}}render() {
    console.log("App Render is called");

    return (
      <div>
        <h2>Current count: {this.state.counter}</h2>
        <button onClick={e= > this.increment()}>+1</button>
        <Header/>
        <Main/>
        <Footer/>
      </div>)}increment() {
    this.setState({
      counter: this.state.counter + 1}}})Copy the code

So, we can think about that in the future development, as long as we modify the data in the App, all components need to render again and perform diff algorithm, and the performance is bound to be low:

  • In fact, many components do not have to be rerendered;
  • They should call render with the premise that their render methods are called when the dependent data (state, props) is changed.

How do you control whether the Render method is called?

  • throughshouldComponentUpdateMethod can;

2.4.2. shouldComponentUpdate

React provides a life cycle method shouldComponentUpdate (in many cases, we call it SCU for short), which takes arguments and returns values:

  • This method takes two parameters:
    • Parameter 1: nextProps Specifies the latest props property after modification
    • Parameter 2: nextState Indicates the latest state attribute after modification
  • The return value of this method is a Boolean type
    • Return true, then call the render method;
    • Return false, so there is no need to call render;
    • The default returns true, meaning that the render method is called whenever state changes;
shouldComponentUpdate(nextProps, nextState) {
  return true;
}
Copy the code

We can control what it returns to determine if it needs to be rerendered.

Let’s add a message property to our App:

  • JSX does not rely on this message, so its changes should not cause rerendering;
  • But because render listens for state changes and rerenders, the render method is called again;
export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0.message: "Hello World"}}render() {
    console.log("App Render is called");

    return (
      <div>
        <h2>Current count: {this.state.counter}</h2>
        <button onClick={e= > this.increment()}>+1</button>
        <button onClick={e= >This.changetext ()}> Change the text</button>
        <Header/>
        <Main/>
        <Footer/>
      </div>)}increment() {
    this.setState({
      counter: this.state.counter + 1})}changeText() {
    this.setState({
      message: "Hello, Li Yinhe."}}})Copy the code

At this point, we can decide whether to call the Render method again by implementing shouldComponentUpdate:

  • At this point, when we change counter, we’re going to re-render it;
  • If we change message, false is returned by default and no rerendering is done;
shouldComponentUpdate(nextProps, nextState) {
  if(nextState.counter ! = =this.state.counter) {
    return true;
  }

  return false;
}
Copy the code

But our code is still not optimized to the best, because when counter changes, all the subcomponents are still re-rendered:

  • So, in fact, we should implement shouldComponentUpdate for all child components;

For example, the Main component can be implemented as follows:

  • shouldComponentUpdateFalse is returned by default;
  • In certain cases, we need to update, we can add the corresponding conditions on the above;
class Main extends Component {

  shouldComponentUpdate(nextProps, nextState) {
    return false;
  }

  render() {
    console.log("Main Render is called");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>)}}Copy the code

2.4.3. PureComponent and memo

If we had to implement shouldComponentUpdate manually for all our classes, that would be a lot more work for us developers.

What is the purpose of the various judgments in shouldComponentUpdate?

  • ShouldComponentUpdate returns true or false if the props or state data has changed;

React actually takes this into account, so React already implements it for us by default. How?

  • Base the class from PureComponent.

For example, if we modify the Main component code:

class Main extends PureComponent {
  render() {
    console.log("Main Render is called");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>)}}Copy the code

How does PureComponent work?

  • A shallow comparison is made between props and state;

Check out the PureComponent source code:

The react/ReactBaseClasses. In js:

  • Add a property of isPureReactComponent true to the PureComponent prototype

The React – reconcilier/ReactFiberClassComponent. Js:

In this method, call! shallowEqual(oldProps, newProps) || ! ShallowEqual (oldState, newState), this shallowEqual is shallow comparison:

What about a functional component?

We need to use a higher-order component memo:

  • We wrap the previous Header, Banner, and ProductList with the memo function.
  • Footer does not use the memo function to wrap;
  • The net effect is that when counter is changed, the Header, Banner, ProductList functions are not re-executed, but the Footer functions are re-executed.
import React, { Component, PureComponent, memo } from 'react';

const MemoHeader = memo(function() {
  console.log("Header Render is called");
  return <h2>Header</h2>
})

class Main extends PureComponent {
  render() {
    console.log("Main Render is called");
    return (
      <div>
        <MemoBanner/>
        <MemoProductList/>
      </div>)}}const MemoBanner = memo(function() {
  console.log("Banner Render called");
  return <div>Banner</div>
})

const MemoProductList = memo(function() {
  console.log("ProductList Render called");
  return (
    <ul>
      <li>Item 1</li>
      <li>2 goods</li>
      <li>Product 3</li>
      <li>Goods 4</li>
      <li>Goods 5</li>
    </ul>)})function Footer() {
  console.log("Footer Render is called");
  return <h2>Footer</h2>
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0.message: "Hello World"}}render() {
    console.log("App Render is called");

    return (
      <div>
        <h2>Current count: {this.state.counter}</h2>
        <button onClick={e= > this.increment()}>+1</button>
        <button onClick={e= >This.changetext ()}> Change the text</button>
        <MemoHeader/>
        <Main/>
        <Footer/>
      </div>)}increment() {
    this.setState({
      counter: this.state.counter + 1})}shouldComponentUpdate(nextProps, nextState) {
    if(nextState.counter ! = =this.state.counter) {
      return true;
    }

    return false;
  }

  changeText() {
    this.setState({
      message: "Hello, Li Yinhe."}}})Copy the code

How does memo work?

The react/memo. Js:

  • Finally returns an object with a compare function in it

2.4.4. Power of immutable data

Let’s rehearse what we said earlier about the importance of immutable data with an example:

import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      friends: [{name: "lilei".age: 20.height: 1.76 },
        { name: "lucy".age: 18.height: 1.65 },
        { name: "tom".age: 30.height: 1.78}}}]render() {
    return (
      <div>
        <h2>Friends list</h2>
        <ul>
          {
            this.state.friends.map((item, index) => {
              return (
                <li key={item.name}>
                  <span>{' name :${item.name} Age :${item. age} '}</span>
                  <button onClick={e= >Enclosing incrementAge (index) + 1} > age</button>
                </li>)})}</ul>
        <button onClick={e= >This.insertfriend ()}> Adds new data</button>
      </div>)}insertFriend(){}incrementAge(index){}}Copy the code

Let’s think about how inertFriend should be implemented. Right?

Implementation method 1:

  • In this way, the interface will not refresh, adding new data;
  • The reason is that it inherits from PureComponent and makes a shallow comparison, in which two Friends are the same object.
insertFriend() {
  this.state.friends.push({name: "why".age: 18.height: 1.88});
  this.setState({
    friends: this.state.friends
  })
}
Copy the code

Implementation method two:

  • [...this.state. Friends, {name: "why", age: 18, height: 1.88}]Generates a new array reference;
  • In shallow comparisons, the two references are different arrays, so they are not the same;
insertFriend() {
  this.setState({
    friends: [...this.state.friends, {name: "why".age: 18.height: 1.88})}}]Copy the code

Let’s think again about how incrementAge should be implemented.

Implementation method 1:

  • Similar to method 1 above
incrementAge(index) {
  this.state.friends[index].age += 1;
  this.setState({
    friends: this.state.friends
  })
}
Copy the code

Implementation method two:

  • Similar to method two above
incrementAge(index) {
  const newFriends = [...this.state.friends];
  newFriends[index].age += 1;
  this.setState({
    friends: newFriends
  })
}
Copy the code

Therefore, in real development, we need to make sure that the data in the state and props are immutable so that we can use PureComponent and Memo properly and safely.

Of course, I will use immutable. Js to ensure data immutability in later projects.

Note: All of this content will be published on our official website. We will update our tutorials on Flutter, TypeScript, React, Node, Uniapp, MPvue, data structures and algorithms, etc. We will also update some of our own learning experiences