Key words: re-render, immutable data, shallow comparison

What is unnecessary (to optimize) re-render

Suppose the following is a React Component tree with no re-render optimization (using the Component class) :

Root component A maintains A state:

A: {b: {e: 0}, C: {g: 0}}Copy the code

Subcomponents B and C of A are props to a.B and a.C, respectively. Subcomponents E of B are props to A.B.E and C are props to A.B.E and A.C, respectively. If we call this.setState() to change the value of A.B.E:

 this.setState(({a}) = > ({
      a: {
        ...a,
        b: {
          e: a.b.e + 1}}}))Copy the code

This only changes A.B.E. it should only need to re-render the component E that depends on A.B.E. and its parent components B and A, but it causes the entire tree including components C and F to be re-rendered. Component C gets a.C unchanged, and component F doesn’t rely on any props, and they shouldn’t be re-rendered.

We use PureComponent to optimize the re-render:

  • For component C, we can usePureComponentInstead ofcomponentCan prevent C from doing unnecessary rerendering,PureComponentShallow comparison of old and newprops.a.cWill only be rerendered if the comparison results are different,props.a.cThe reference has not changed so component C will not be rerendered.
  • The same is true for component F and component CPureComponentCan be

ShouldComponentUpdate returns true. If parent components A and B are rerendered, child components C and F should also be rerendered. PureComponent rewrites shouldComponentUpdate to use shallow comparisons to compare the old and new props with the old and new state. As long as the references to state and props are unchanged, the component doesn’t re-render.

In addition to using PureComponent, we can use shouldComponentUpdate and react.Memo () to do the same optimization. ShouldComponentUpdate requires us to customize shallow comparison logic. The first argument to react.memo () is the component we want to reuse in the cache. The second argument is a comparison function. In this comparison function, we can control whether or not the react.Memo () component needs to be updated. If true, then react.Memo () returns the component that was evaluated last time. If we don’t pass the react.Memo () second argument, shallow contrast is used by default, so we just use react.Memo (C) and the optimization is the same as if component C inherited PureComponent.

Note that we can successfully optimize re-render using PureComponent only if we write the state modification in the this.setstate () method above correctly.

     const _e = this.state.a.b.e + 1
     this.setState({
       a: {
         b: {
           e: _e
         },
         c: {
           g: 0}}})Copy the code

Although the value of a.c remains the same, changing its reference to create a completely new A.C will cause component C to re-render, even though we used PureComponent in component C.

It is worth mentioning that there are several vivid scenarios in which PureComponent functions are passed from the parent component to the child component. Although the props of the parent component are not modified, a new one is created each time the parent component is re-rendered, resulting in unnecessary rendering of the child component, such as component B below. It passes the style to F, and the value of the style is constant, but every time B is updated, the props. Style of the child component is new, so F is updated unnecessarily.

class B extends React.PureComponent {
  render () {
  	return <F style={{width:'400px', height: '300px'}} / >}}Class B extends react. PureComponent {render () {style = {width:'400px', height: '300px'} return 
      
        } } */
      

class F extends React.PureComponent {
  render(){
	return <div style={this.props.style}>Component F</div>}}Copy the code

In this case, we can initialize B style as a component property, instead of putting it in render, which causes B render to assign style every time. F-rerender can be avoided like this:

class B extends React.PureComponent {
  style = {width:'400px'.height: '300px'}
  render () {
  	return <F style={this.style}/>}}class F extends React.PureComponent {
  render(){
	return <div style={this.props.style}>Component F</div>}}Copy the code


this.setState({()=>this.setState({… })}/>, then if component B is re-rendered, onChang will also rebuild a new method, causing component F to be unnecessarily re-rendered. The solution is to initialize the definition of the onChange method as a property of component B:

class B extends React.PureComponent {
  change= () = > {
  	this.setState({... }) } render () {return <F onChange={this.change}/>}}Copy the code

Component B will not create a new onChange method when rerendering, so F will not rerender.

If component B is written as a function component:

function B () {
   const change= () = > {
  	this.setState({...})
   }
   return <F onChange={this.change}/>
}
Copy the code

Component B will be re-rendered, and F will be re-rendered, because the change method will be reconstructed when B is re-rendered. We can use useCallback to optimize:

import {usecallback} from 'react'

function B () {
   const change = useCallback(() = > {
  	this.setState({...})
   }, [])
   return <F onChange={this.change}/>
}
Copy the code

We pass an empty array to the second parameter of useCallback, so that each time B is rerendered, useCallback returns the change method that was initialized the first time, rather than recalculating a new method and not causing component F to be rerendered.

Why does React require state not to be changed directly

React applications often use immutable data and PureComponent (shallow comparisons) for performance optimization. The React setState() method requires that the state cannot be changed directly, such as this.state.a.b.d.e ++. Instead, you should pass in a newState using setState(newState). Because updating the state updates the component, and if each state update updates the component once, and then passes through the diff of the old and new component trees before rendering to the DOM, then you end up with a lot of re-render, Every time re-render also with diff, application performance can be imagined.

React has a batch update state strategy in order to optimize the multiple DOM rerender caused by multiple updates of components. In this strategy, multiple setstates in the same batch update phase are merged into one, and the state is updated asynchronously, so that only one re-render is available at the end.

With a design like React, if we change the state directly, it’s likely to be overwritten by setState, because setState might be asynchronous.

Also suppose a component uses a PureComponent, if its state is a reference data structure, for example:

A: {b: {e: 0}, C: {g: 0}}Copy the code

When we modify A.B.E, we cannot modify it like this:

   const _a = this.state.a
   _a.b.e ++
   this.setState({
     a: _a
   })
Copy the code

The write component will not be updated (because the shallow comparison state.a reference has not changed).

Let’s list some ways to modify state and compare their strengths and weaknesses

In order to optimize re-render with PureComponent shallow comparisons, state cannot be changed directly. In order not to change the original state, we write the following:

1. Use deep copy to copy the state, modify the secondary value, and pass the secondary value as the new state to setState:

    import _ from 'lodash'

    const _a = _.cloneDeep(this.state.a)
    _a.b.e ++
     this.setState({
       a: _a
     })

Copy the code

Let’s look at the new and old state printed out:

Although this method implements immutable state, the deep copy generates new state, which disconnects the reference of the modified part and also disconnects the reference of the unmodified part. We only want to modify A.B.E. We only need to re-render components A, B, and E, but component C has also been re-rendered. Since the deep copy also changes the reference to a.c, PureComponent’s shallow comparison shows that A.C has changed, so it is rerendered.

So not only is the deep-copy method inherently inefficient, it may also cause the React PureComponent performance optimization to fail.

2. Use.Object.asssing()Only modify the parts that need to be modified, and the parts that are not modified will share references with the original

 this.setState(({a}) = > ({
      a: {
        ...a,
        b: {
          ...a.b,
          e: a.b.e + 1}}}))Copy the code

In this way, we return the new state, modify A.B.E, and other parts such as A.c and a.B will share references with the old state’s a.C and a.B, respectively, without causing component C to re-render. But it’s easy to write spaghetti code this way, and you can’t tell where you’ve changed it.

3. Use immer.js immutable data structure writing to modify state

Immer.js is used to implement JS immutable data structures. What is immutable data? When working with reference data, in some cases we may need to generate new data based on it without changing the original data. This is immutable data.

Conventional writing:

   const nextState = produce(this.state, draftState= > {
     draftState.a.b.e ++
   })
   this.setState(nextState)
Copy the code

More succinctly:

 this.setState(produce(draftState= >{
   draftState.a.b.e ++
 }))
Copy the code

Using immer. js, you can save the modified part in the new memory, so that the modification will not cause the corresponding part of the original data to be modified, and the unmodified part and the original data keep the same block reference. This prevents data from being modified for reference types in state that do not want to be modified, causing unnecessary rendering of unrelated components. Immer’s structure-sharing feature of deeply nested objects and React’s PureComponent combine to perfectly optimize Re-render.

We can verify this property of immer by defining an object obj with two internal objects obj. A and obj. B. Obj.a.e.f and obj.b.c.d are then modified directly without immer. As you can see from the printed result, newobj.a.e.f is unchanged and newobj.b.c.d is modified. Note Newobj.a.e.f that is modified by immer is disconnected from obj.a.e.f, but newobj.b.c.d that is not modified by immer remains referenced to obj.B.c.d.

var immer = require("immer")
const obj = {
  a: {e: {
     f: 1}},b: {c: {d:2}}}// Use immer to modify obj.a.e.f
const newObj = immer.produce(obj, draftObj= >{draftObj.a.e.f = 0})

console.log({obj, newObj})
/* newObj: { a: {e: {f: 0}} b: {c: {d: 2}} } obj: { a: {e: {f: 1}} b: {c: {d: 2}} } */

obj.a.e.f = 7 // Modify parts that have already been modified with immer
obj.b.c.d = 3 // Modify parts that have not been modified by IMmer

console.log({obj, newObj})
/* newObj: { a: {e: {f: 0}} b: {c: {d: 3}} } obj:{ a: {e: {f: 7}} b: {c: {d: 3}} } */
Copy the code

There are several tools to help optimize React re-render: Welldone -software/ Why-did-you-render is a tool that tracks the React component rendering information and prints the cause of unnecessary rerendering of irrelevant components to the console, so you can optimize those areas accordingly. Also, if you use React-devTools, you can turn on the Highlight Updates when Components Render option so that you can visually see which components have been updated on the page, with a green border flashing if the components have been updated.

The pure function reducer in Redux also requires immutable data

Reducer in Redux is a pure function and cannot perform operations with side effects. Like React setState, we need to return a new store to update the store, rather than making changes on the old store. Redux also uses shallow object comparisons to optimize performance. In Redux we need to listen for store updates (store.subscribe(listener)) before we can notify the UI of updates. In this implementation, we need to know if the store has changed. If the store is a deeply nested object, then the normal way to determine whether the store has changed is to compare the store deeply. Naturally, this method is very performance consuming. Therefore, Redux did not choose such a method, but retained the old store and returned the new store from the Reducer like React. Redux can know whether the store is updated by simply comparing the old and new stores. So we’ll see that Redux recommends that developers use {… oldStore, … NewStore}, object.assing ({}, oldStore, newStore) to get back to the newStore.

This explains in detail why immutable data is needed in Redux and React-Redux. Here is a brief note transcribed from it:

Why does Redux need immutability?

  • Both Redux and React-Redux use shallow comparisons. To be specific:

The combineReducers method of Redux shallow compares whether the reducer references it called have changed. The components generated by the React-Redux connect method shallow compare the reference changes of the root state to the return value of the mapStateToProps function to determine whether the wrapped component needs to be rerendered. The above shallow comparison requires invariance to work properly.

  • Immutable data management greatly improves the security of data processing.
  • Time travel debugging requires that the reducer be a pure function with no side effects to move correctly between different states.

Therefore, immutable data is also needed to update the reducer store. In this case, immer can also be used to optimize the Reducer in the most concise way.

Reducer without immer

const byId = (state = {}, action) = > {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            return{... state, ... action.products.reduce((obj, product) = > {
                    obj[product.id] = product
                    return obj
                }, {})
            }
        default:
            return state
    }
}
Copy the code

Reducer of IMmer is more brief and easy to understand, and only needs to focus on what needs to be modified in the state:

import produce from "immer"

const byId = produce((draft, action) = > {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            action.products.forEach(product= > {
                draft[product.id] = product
            })
    }
}, {})
Copy the code

How to avoid re-render in Redux applications

The state of the entire Redux application is ultimately managed by a store. Then, in the component, we can use the store.getState() method to get the required state of the component. In order to update the component after the state changes, You can store state in the component’s state using setState(), but this method duplicates the state cache, so you can wrap a container component around the component and get Redux’s store in the container component. Pass the state to the packaged component via props, so that the state changes and the component is updated. That’s what React-Redux’s Connect does. Each time the Store changes, the mapStateToProps is recalculated, but maybe the state required by the component hasn’t changed, so this part of the recalculation isn’t necessary. If you need to perform computations with a lot of derived data in mapStateToProps, it is likely that those computations will affect performance. To solve this problem, we can use Reselector to cache the first calculation result in mapStateToProps. When the root store updates, if the required state does not change, we can reuse the previously cached calculation result to reduce the calculation of the derived data.

Here we have two components
and
that rely on com1State and com2State in the root store for rendering, respectively. Count1 = store.1state. count + 1 in mapStateToProps Finally render count1 to
. For
, although com2state.count does not change, But the root store has been updated, so the mapStateToProps of
will recalculate count2 = store.2state.count + 1, even though the result is the same as before. If count2 = store.2state.count + 1 is replaced with a more complex calculation, this unnecessary double-counting process affects application performance.

MapStateToProps is as follows:

const getCount = (store) = > { 
  const count2 = store.com2State.count + 1 // Every store update is executed
  return count2
}

const mapStateToProps = (store) = > {
	return {
    	count2: getCount(store)
    }
}

Copy the code

Now use Reselector to optimize the calculation logic of derived data. We rewrite the above code into the following form:

import { createSelector } from 'reselect'

const selectCom2Count = createSelector(
  store= > store.com2State,
  com2State= > { // The result of the derived data calculation is cached, and only the com2State update is recalculated
    const count2 = com2State.count + 1
    return count2
  }
)

const mapStateToProps = (store) = > {
	return {
    	count2: selectCom2Count(store)
    }
}
Copy the code

Now when store is updated, but store.2state is not updated, the derived data calculation will not be performed, naturally avoiding some unnecessary calculation.

How does Reselector determine if the required state of a component is updated? Here’s how Reselector works (without the source code) :

Take the following passage:

// selector
const selectCom2Count = createSelector(
  store= > store.com2State, // First inputSelector
  com2State= > {  // Second inputSelector
    const count2 = com2State.count + 1
    return count2
  }
)
Copy the code

So the two arrow functions that are passed in createSelector we call them inputSelector functions, and reselector internally uses closures to cache the return values of those inputSelector functions, Only when the number of these inputSelector functions changes, or an inputSelector parameter changes, does the inputSelector function update the cache again. Again, shallow comparisons, reselector will shallow compare the arguments to inputSelector, for example, the second inputSelector above, and if the reference to com2State hasn’t changed, it’s not updated, Count2 then reuses the result of the previous calculation cache, and does not perform a second inputSelector causing a double calculation.