Why do we need ResELECT

Problems encountered

Take a look at one of the components below

import React, { Component } from 'react'
import { connect } from 'react-redux'

class UnusedComp extends Component {
    render() {
        const { a, b, c, fab, hbc, gac, uabc } = this.props
        return (
            <div>
                <h6>{a}</h6>
                <h6>{b}</h6>
                <h6>{c}</h6>
                <h6>{fab}</h6>
                <h6>{hbc}</h6>
                <h6>{gac}</h6>
                <h6>{uabc}</h6>
            </div>)}}function f(x, y) {
    return a + b
}

function h(x, y) {
    return x + 2 * y
}

function g(x, y) {
    return 2 * x + y
}

function u(x, y, z) {
    return x + y + z
}Copy the code

The UnusedComp component cares about several props: A, B, C, f(a,b), H (b, c), G (a, C), u(a, b, C), where f, H, g, and u are functions, respectively. What do we do with these calculated values?

Calculate the data directly in redux

First, we store all the values in redux, and the structure of all stores looks something like this:

store = {
    a:1.b:1.c:1.fab: 2.// a + b
    hbc: 3.// b + 2c
    gac: 3.// 2a + c
    uabc: 3 // a + b + c
}Copy the code

Const {a, b, c, fab, HBC, gac, uabc} = this.props So, what should be done with the reducer function? The corresponding ones are as follows:

switch(action.type) {
    case 'changeA': {
        return {
            ...state,
            a: action.a,
            fab: f(action.a, state.b),
            gac: g(action.a, state.c)
            uabc: u(action.a, state.b, state.c)
        }
    }
    case 'changeB': {... }case 'changeC': {... }}Copy the code

Our Reducer function was very complicated and we updated each state value. You have to maintain a value that is associated with this value, otherwise there will be data inconsistencies.

The Reducer saves only the most basic state

In order to ensure clear data flow, simple update. We store only the most basic state in Redux. The store structure and redcuer functions are as follows:

store = {
    a:1.b:1.c:1,}... switch(action.type) {case 'changeA': {
        return {
            ...state,
            a: action.a
        }
    }
    ...
}Copy the code

At the moment the component might look like this:

class UnusedComp extends Component {
    render() {
        const { a, b, c } = this.props
        const fab = f(a, b)
        const hbc = h(b, c)
        const gac = g(a, c)
        const uabc = u(a, b, c)
        return (
            <div>
                <h6>{a}</h6>
                <h6>{b}</h6>
                <h6>{c}</h6>
                <h6>{fab}</h6>
                <h6>{hbc}</h6>
                <h6>{gac}</h6>
                <h6>{uabc}</h6>
            </div>)}}Copy the code

Or this:

class UnusedComp extends Component {
    componentWillReciveProps(nextProps) {
        const { a, b, c } = this.props
        this.fab = f(a, b)
        this.hbc = h(b, c)
        this.gac = g(a, c)
        this.uabc = u(a, b, c)
    }


    render() {
        const { a, b, c } = this.props
        return (
            <div>
                <h6>{a}</h6>
                <h6>{b}</h6>
                <h6>{c}</h6>
                <h6>{this.fab}</h6>
                <h6>{this.hbc}</h6>
                <h6>{this.gac}</h6>
                <h6>{this.uabc}</h6>
            </div>)}}Copy the code

In the first case, the calculation is performed when the component ownProps (component properties, not passed by REdux), or setState. In the second case, the calculation is performed when the component ownProps changes. And both violate our basic principle: keep component logic simple

Get the data logic out of the component!

// Can be written as a functional component
class UnusedComp extends Component {
    render() {
        const { a, b, c, fab, hbc, gac, uabc } = this.props
        return (
            <div>
                <h6>{a}</h6>
                <h6>{b}</h6>
                <h6>{c}</h6>
                <h6>{fab}</h6>
                <h6>{hbc}</h6>
                <h6>{gac}</h6>
                <h6>{uabc}</h6>
            </div>)}}function mapStateToProps(state) {
    const {a, b, c} = state
    return {
        a,
        b,
        c,
        fab: f(a,b),
        hbc: h(b,c),
        gac: g(a,c),
        uabc: u(a, b, c)
    }
}
UnusedComp = connect(mapStateToProps)(UnusedComp)Copy the code

The component is simple enough to receive the data presentation. It looks beautiful! We know that all connect components are notified when store data is changed (provided it is not destroyed). Assuming that there are three components A, B and C on the page, any change in the state of these three components (redux state) will trigger the execution of f, H, G and U here… Sounds crazy!! That’s crazy! (In Redner, the calculation in willReciveProps is that this does not cause the function to execute). But this is usually not a problem because we typically only have one container component per page that interacts with Redux, and the other subcomponents get data and actions via props. In addition, the React-router destroys the components of the previous route when switching routes. There will be only one container component at a time.

Let’s consider the case where UnusedComp and the x, y, z state properties exist redux. These three properties are simply three values, just for demonstration purposes. But when x, y, and z change, it also triggers the calculation. The calculation that happens here is not avoidable in render, willReciveProps, or mapStateToProps.

Precise control calculation

As if we had found a way:

  1. Redux stores only base states
  2. React-router + single-container component component

The reality is cruel! In fact, properties like x, y, and z must exist in abundance. This alone leads to a lot of invalid calculations. The three approaches discussed earlier (Render, willRecive, and mapStateToProps) cannot avoid this calculation.

The mapStateToProps can also be affected by changes to the values of other stores, because the React-Router + single-container component is a best-case scenario. Some of our businesses are simply for performance reasons, using our own routing instead of destroying previously routed components. Some pages are also not single-container components, embarrassing!!

Obviously, we know that changes in x, y, and z are not computable, but changes in A, B, and C are computable. How do I describe it to the program? Another benefit of this approach is that mapStateToProps is not intrusive when describing components!! .

Original description:

let memoizeState = null
function mapStateToProps(state) {
    const {a, b, c} = state
    if(! memoizeState) { memoizeState = { a, b, c,fab: f(a,b),
            hbc: h(b,c),
            gac: g(a,c),
            uabc: u(a, b, c)
        }
    } else {
        if(! (a === memoizeState.a && b === memoizeState.b) ) {// f should invoke
            memoizeState.fab = f(a, b)
        }
        if(! (b === memoizeState.b && c === memoizeState.c) ) {// h should invoke
            memoizeState.hbc = h(b, c)
        }
        if(! (a === memoizeState.a && c === memoizeState.c) ) {// g should invoke
            memoizeState.gac = g(a, c)
        }
        if(! (a === memoizeState.a && b === memoizeState.b && c === memoizeState.c) ) {// u should invoke
            memoizeState.uabc = u(a, b, c)
        }
        memoizeState.a = a
        memoizeState.b = b
        memoizeState.c = c
    }

    return memoizeState
}Copy the code

First, we know that fab values are associated with a and B, so when a and B change, f needs to be re-executed. Similarly, the function must be executed only when necessary.

Using reselect

Reselect solves our above problem, and we don’t have to use the original description every time, the corresponding Reselect description looks like this

import { createSelector } from 'reselect'

fSelector = createSelector(
    a= > state.a,
    b => state.b,
    (a, b) => f(a, b)
)
hSelector = createSelector(
    b= > state.b,
    c => state.c,
    (b, c) => h(b, c)
)
gSelector =  createSelector(
    a= > state.a,
    c => state.c,
    (a, c) => g(a, c)
)
uSelector = createSelector(
    a= > state.a,
    b => state.b,
    c => state.c,
    (a, b, c) => u(a, b, c)
)

...
function mapStateToProps(state) {
    const { a, b, c } = state
    return {
        a,
        b,
        c,
        fab: fSelector(state),
        hbc: hSelector(state),
        gac: gSelector(state),
        uabc: uSelector(state)
    }
}Copy the code

In createSelector we define the input-Selector function first, and then we define how the value is evaluated. Selector guarantees that when an input-selector returns an equal result, it’s not evaluated.

The last

If you are a React-Router and a single-container component. So maybe inside mapStateToProps, the performance isn’t that bad. And performance shouldn’t be the first thing to worry about, simplicity should be the first thing to worry about, especially component simplicity. Reselect is a great choice for us when our business is complex enough to consider performance!