Ant Financial · Data Experience Technology team

Recently in the process of requirement development, I have stepped on a lot of pit because update reference data but the page does not re-render, so I summarized the content of this section.

Before we talk about Immutable data, let’s talk about how the React component renders updates.

Update mode of the React component

A direct change to state

The React component update is caused by a change in the state of the component. The state usually refers to the state object in the component. When the state of a component changes, the component will undergo the following process during update:

  • shouldComponentUpdate
  • componentWillUpdate
  • render()
  • componentDidUpdate

State is usually updated by performing this.setState inside the component. SetState is an asynchronous operation that simply executes the state to be modified in an execution queue. Merge multiple setState operations into a single execution.

The change of the props

In addition to the state that causes the component to update, the props passed in from outside also causes the component to update, but this is when the child component renders directly using the parent component’s props, for example:

render(){
	return <span>{this.props.text}</span>
}

Copy the code

When the props are updated, the subcomponents will render the updates, which run in the following order:

  • componentWillReceiveProps (nextProps)
  • static getDerivedStateFromProps()
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • getSnapshotBeforeUpdate()
  • componentDidUpdate

The React component’s life cycle runs in sequence, as shown in the sample output.

Indirect change of state

Another is to convert props to the state to render the component, if update props, to make a component to render requires the latest props in componentWillReceiveProps lifecycle assigned to the state, such as:

class Example extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            text: props.text
        };
    }
    componentWillReceiveProps(nextProps) {
        this.setState({text: nextProps.text});
    }
    render() {
        return <div>{this.state.text}</div>}}Copy the code

Updates in this case are also a variant of setState, with a different source of state.

React component update process

When a React component is updated (the state or props changes), React builds a new Virtual DOM tree based on the new state. Then the diff algorithm is used to compare the Virtual DOM with the previous one and re-render it if it is different. React shouldComponentUpdate (shouldComponentUpdate) should return true before rendering. So if any position in the component changes, the rest of the component will be rerendered.

When a component to render the agency is very simple, because such a state change caused by changes in the components may not, but when the component rendering is very complex, a lot of node tree components, for example, when changing a leaf node state, the whole tree will apply colours to a drawing, even those who do not update status nodes, This costs performance to some extent, resulting in slower rendering and updating of the entire component, which affects the user experience.

Shallow comparison of PureComponent

React also introduces PureComponent, which has a PureRenderMixin plugin. The PureRenderMixin plugin implements the shouldComponentUpdate method, which performs a shallow comparison as follows:

function shallowCompare(instance, nextProps, nextState) {
  return(! shallowEqual(instance.props, nextProps) || ! shallowEqual(instance.state, nextState) ); }Copy the code

The PureComponent logic for determining whether an update is needed is the same as the PureRenderMixin plugin. The source code is as follows:

 if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = ! shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState); }Copy the code

Using the above two methods can avoid unnecessary re-rendering of unchanged elements, but using the above shallow comparison can cause some problems:

Suppose the props data structure passed to a component looks like this:

const data = {
   list: [{
      name: 'aaa'.sex: 'man'}, {name: 'bbb'.sex: 'woman'}].status: true,}Copy the code

If you change a field and expect the component to be rendered again, you will find that the component is not rendered again because the changed data uses the same memory as the original data. The result of all comparisons is always false, resulting in the component not being re-rendered.

Several ways to solve the problem

To solve this problem, we need to consider how to use Immutable data when updating reference data.

Use a deep copy of Lodash

The code for this approach is as follows:

import _ from "lodash";

 const data = {
	  list: [{
	    name: 'aaa'.sex: 'man'
	  }, {
	    name: 'bbb'.sex: 'woman'}].status: true,}const newData = _.cloneDeepWith(data);
 shallowEqual(data, newData) //false
 
 // Change one of the fields to compare
  newData.list[0].name = 'ccc';
  shallowEqual(data.list, newData.list)  //false

Copy the code

This is done by making a deep copy of a complex type and then changing one of its values, so that the two use different reference addresses and the comparison returns false, but the drawback is that this deep-copy implementation is memory intensive.

Using JSON. Stringify ()

This method is equivalent to a kind of dark magic, and can be used as follows:


  const data = {
    list: [{
      name: 'aaa'.sex: 'man'
    }, {
      name: 'bbb'.sex: 'woman'}].status: true.c: function(){
      console.log('aaa')}}const newData = JSON.parse(JSON.stringify(data))
 shallowEqual(data, newData) //false
 
  // Change one of the fields to compare
  newData.list[0].name = 'ccc';
  shallowEqual(data.list, newData.list)  //false
Copy the code

This approach is a variant of deep copy, and it has the same disadvantages as the one above. The other two disadvantages are that if you have a function in your object, the function cannot be copied, nor can you copy the properties and methods in the copyObj prototype chain

Destructuring with Object

Object deconstruction is ES6 syntax, first on the code analysis:

  const data = {
    list: [{
      name: 'aaa'.sex: 'man'
    }, {
      name: 'bbb'.sex: 'woman'}].status: true,}constnewData = {... data};console.log(shallowEqual(data, newData));  //false
  
  console.log(shallowEqual(data, newData));  //true
  // Add a field
  newData.status = false;
  console.log(shallowEqual(data, newData));  //false
  // Modify a field of a complex type
  newData.list[0].name = 'abbb';
  console.log(shallowEqual(data, newData));  //true
Copy the code

As you can see from the above tests, destructuring works when modifying variables of simple types in the data, but it does not detect when modifying complex types (there was a big hole).

Because deconstruction is compiled by Babel as object.assign (), but it is a shallow copy, represented as follows:

The downside of this approach is obvious: updates cannot be detected for complex types of data.

Use third-party libraries

Several libraries are available to solve this problem, such as immutability-helper, immutable, or immutability-helper-x.

immutability-helper

A library based on Array and Object operations, just a file but very easy to use. For example, the above example could be written as follows:

   import update from 'immutability-helper';
    
    const data = {
    list: [{
      name: 'aaa'.sex: 'man'
    }, {
      name: 'bbb'.sex: 'woman'}].status: true,}const newData = update(data, { list: { 0: { name: { $set: "bbb"}}}});console.log(this.shallowEqual(data, newData));  //false

   // When only the following changes occur
   const newData = update(data,{status: {$set: false}});
   console.log(this.shallowEqual(data, newData));  //false
   console.log(this.shallowEqual(data.list, newData.list));  //true
Copy the code

At the same time, it can be found that when only the status field in data is changed, the reference fields of the two before and after are compared and found to be shared memory, which saves memory consumption to a certain extent. And the API is familiar with some of the Array and Object operations, relatively easy to use.

immutable

Immutability helper is much more powerful than immutability helper, but at the same time, immutable is also a way to increase the cost of learning because you need to learn new apis.

immutability-helper-x

Finally, immutability helper-x is another open source library

const newData = update(data, { list: { 0: { name: { $set: "bbb"}}}});Copy the code

Simplify to something more readable

const newData = update.$set(data, 'list.0.name'."bbb");
或者
const newData = update.$set(data, ['list'.'0'.'name']."bbb");
Copy the code

Write in the last

In React projects, it is best to use immutable data to avoid many bugs that you don’t know about. The above is just some summary and accumulation in the actual development of the individual, if there is a wrong place to welcome clap brick ~

If you are interested in our team, you can follow our column, follow Github or send your resume to ‘tao.qit####alibaba-inc.com’.replace(‘####’, ‘@’)

Original address: github.com/ProtoTeam/b…