Today we will solve the following questions: what is immutable data, what problems immutable data brings, and what performance is optimized by immutable data?

Mutable data Variability of data

The variability of data can be described in a single piece of code

const a = [{ todo: 'Learn js'}, {todo: 'Learn react'}];
const b = [...a];
b[1].todo = 'Learn vue';
console.log(a[1].todo); //Learn vue
Copy the code

In fact, we can see at a glance that this is the problem caused by shallow copy. The inner objects point to the same heap memory location, so if you modify the objects in array B, array A will also change. Usually when we operate complex data structures in projects, we are used to deepCopy, otherwise some undetectable bugs will appear.

We can use deepCopy whenever we need to operate on multi-layer data structures. However, the fact that deepCopy costs performance is obvious as the number of data layers increases, and deepCopy in React causes a lot of overhead due to React’s rendering mechanism.

What performance is optimized for immutable data

First we need to look at how React renders.

React rendering mechanism resolution

Graph LR setState or props change --> shouldComponentUpdate --> render --> componentDidUpdate

In React, the render function returns the virtual DOM tree, calculates the difference with the last virtual DOM through Diff algorithm, and updates the difference part to render the real DOM.

If shouldComponentUpdate returns false, the update process will be interrupted, so we should use this shouldComponentUpdate.

shouldComponentUpdate

This is a subtree of a component. In each node, SCU represents the value returned by shouldComponentUpdate, and vDOMEq represents whether the React elements returned are the same. Finally, the color of the circle indicates whether the component needs to be mediated. Red means shouldComponentUpdate returns true, render, and green means return false, render without.

C1 is the red node, shouldComponentUpdate returns true, enter the diff algorithm to compare the old and new VDom trees, if the new and old VDom trees node type is different, then replace all, including the lower face component, the figure shows the case of the same node type, then recursive sub-components.

// What are different node types
<A>
  <C/>
</A>
// A and B are of different node types
<B>
  <C/>
</B>React deletes A node (including all its children) and creates A new B node.Copy the code

C2’s shouldComponentUpdate returns false, so React will not call C2’s shouldComponentUpdate, so C4 and C5’s shouldComponentUpdate will not be called.

C3, shouldComponentUpdate returns true, so React needs to continue querying the child nodes. Here C6’s shouldComponentUpdate returns true, and React updates the DOM because the elements are rendered differently than before.

The last interesting example is C8. React needs to render this component, but since it returns the same React elements as previously rendered, there is no need to update the DOM.

Obviously, you see that React only changes the DOM for C6. For C8, the React element is skipped by comparing the render. For C2’s children and C7, render is not called because of shouldComponentUpdate. So they don’t need a contrast element.

Class component React.PureComponent and function component memo

ShouldComponentUpdate can avoid unnecessary rendering process, so as to achieve performance optimization. If we need to compare each of the props and state properties, it would be too much of a problem. React provides two ways to do that automatically. The function component provides the memo method.

// Three ways
class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color ! == nextProps.color) {return true;
    }
    if (this.state.count ! == nextState.count) {return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={()= > this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>); }}class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={()= > this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>); }}const CounterButton = props= > {
  const [count, setCount] = useState(1);

  return (
    <button color={props.color} onClick={()= > setCount(count => count + 1)}>
      Count: {count}
    </button>
  );
};

export default React.memo(CounterButton);

Copy the code

Note: Both React.PureComponent and Memo only perform shallow comparisons. If the attribute value is a reference type, the shallow comparisons will be invalidated due to the variability of the data explained above. If we use deepCopy, then all of our shouldComponentUpdate components will return true, and they’re all diff, so it’s meaningless.

Is there a way to use a light comparison to figure out which part is the changed data node?

Immutable data structures

Once again, we know what is immutable by a little piece of code, using the Immer library.

  import produce from 'immer';
  
  const a = [{ todo: 'Learn js' }, { todo: 'Learn react' }];
  const b = produce(a, draftState= > {
    draftState[1].todo = 'Learn vue';
  });

  console.log(a === b); //false
  console.log(a[0] === b[0]); //true
  console.log(a[1] === b[1]); //false
  console.log(a[1].todo === b[1].todo); //false

Copy the code

Here you can see that the memory address of the unchanged reference type is not changed, ensuring that the old node is available and unchanged, while the node that has changed is updated, along with all its associated parent nodes. As shown in the figure:

This avoids the huge performance overhead associated with deep copies, and the update returns a completely new reference so that even shallow comparisons can sense what part of the data needs to be updated.

Example of immer application

const [state, setState] = useState({
    id: 14.email: "[email protected]".profile: {
      name: "Stewie Griffin".bio: "You know, the... the novel you've been working on".age:1}});function changeBio(newBio) {
    setState(current= > ({
      ...current,
      profile: {
        ...current.profile,
        bio: newBio
      }
    }));
  }



/ / use immer
import { useImmer } from 'use-immer';

const [state, setState] = useImmer({
    id: 14.email: "[email protected]".profile: {
      name: "Stewie Griffin".bio: "You know, the... the novel you've been working on".age:1}});function changeBio(newBio) {
   setState(draft= > {
      draft.profile.bio = newBio;
    });
  }
Copy the code

It’s a lot cleaner to have less deconstructed syntax, but the immer advantage will increase as the data structure becomes more complex.

Thank you for reading.