I’ve known about reselect for a long time, which caches the results of selector calculations to avoid unnecessary double-counting and can be used for performance optimization. Due to some factors such as time, it has not been applied to the actual project. As the project grows, the number of interface requests on the page increases, causing many performance-related problems. Especially for mobile apps, users have been feedback that app starts slowly and page jumps slowly. Last December, I was called in to do some work related to performance optimization when the project was undergoing major changes. During the whole process, I reviewed the documents related to ResELECT and found some related discussions on Git, which generated a lot of thoughts about React, Redux, React-Redux and ResELECT. I wanted to write them down and share them with you. I hope you found this article useful.

This paper mainly focuses on the problem of “interface rendering caused by irrelevant data changes in store”, and analyzes the causes of this problem by combining the source code of React-Redux, and gives relevant solutions. At the same time, some limitations of ResELECT in practice are discussed. Finally, the concept of per-component Memoization and how to take advantage of this feature of React-Redux for performance optimization was introduced. Finally, the article also interspersed with some basic questions about “where to do connect”, “what data model should be adopted in store” and so on.

First, start with the problem

1. Scenario description

The store structure is assumed as follows:


The interface is as follows:


Where, demo_1 and demoe_2 are reducer keys, CounterView_1 and CounterView_2 display counter values in DEMO_1 and demo_2 respectively through connect. When clicking Button 1 and Button 2, update the counter value of demo_1 and demo_2 with DEMO_ACTION_1 and DEMO_ACTION_2 respectively.

2. Expect results

When Button 1 is clicked, the demo_1 counter value is incremented by one, CounterView_1 is re-rendered to show the latest counter value, CounterView_2 is not re-rendered (because the demo_2 counter value has not changed); And vice versa

3. Partial code implementation

The SRC file directory structure is as follows:

./ SRC └ ─ app.CSS └ ─ app.js └ ─ Counterview2.js └ ─ Counterview2.js └ ─ Index.css └ ─ Index.js ├── logo.svg ├── Class ├─ class Exercises ── Class Exercises ── Class Exercises ── DemoReducer_2. Js └ ─ ─ reducers. JsCopy the code

View code is as follows:

// // src/app.js // import React, { Component } from 'react' import { createAction } from 'redux-actions' import logo from './logo.svg' import * as actionTypes from './constants' import CounterView1 from './CounterView1' import CounterView2 from './CounterView2' import { getStore } from './index' class App extends Component { render() { console.log('...... App props:', this.props) return ( <div style={{ marginTop: 20, marginLeft: 20 }}> <button onClick={this.buttonEvent_1}> Button 1 </button> <button onClick={this.buttonEvent_2} style={{ marginLeft: 20 }} > Button 2 </button> <CounterView1 /> <CounterView2 /> </div> ) } buttonEvent_1 = () => { let store = getStore() if (! store) { console.log('buttonEvent_1') return } store.dispatch(createAction(actionTypes.DEMO_ACTION_1)({})) } buttonEvent_2 = () => { let store = getStore() if (! store) { console.log('buttonEvent_2') return } store.dispatch(createAction(actionTypes.DEMO_ACTION_2)({})) } } export default App; // // src/CounterView1.js // import React, { Component } from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' class CounterView1 extends Component { render () { console.log('...... CounterView1 props:', this.props) return ( <div style={{ backgroundColor: '#00ff00', width: 300, height: 100 }}> <h1 className="App-title">CounterView 1: } {this. Props. DemoData. Counter < / h1 > < / div >)}} const mapStateToProps = (state) = > {/ / copy demo_1 directly modified in order to avoid store let demo_1 = Object.assign({}, state.demo_1) if (! Demo_1.counter) {demo_1.counter = 0} // Considering the complexity of actual application scenarios, demoData_1, demoData_2... Return {demoData: demo_1 } } const mapDispatchToPeops = (dispatch) => { return bindActionCreators({}, dispatch) } export default connect(mapStateToProps, mapDispatchToPeops)(CounterView1) // // src/CounterView2.js // import React, { Component } from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' class CounterView2 extends Component { render () { console.log('...... CounterView2 props:', this.props) return ( <div style={{ backgroundColor: '#ff0000', width: 300, height: 100 }}> <h1 className="App-title">CounterView 2: {this.props.demoData.counter}</h1> </div> ) } } const mapStateToProps = (state) => { let demo_2 = Object.assign({}, state.demo_2) if (! demo_2.counter) { demo_2.counter = 0 } return { demoData: demo_2 } } const mapDispatchToPeops = (dispatch) => { return bindActionCreators({}, dispatch) } export default connect(mapStateToProps, mapDispatchToPeops)(CounterView2)Copy the code

The reducer code is as follows:

// src/store/demoReducer_1.js import * as actionTypes from '.. /constants' export default function reducer (state = {}, action) { switch (action.type) { case actionTypes.DEMO_ACTION_1: return handleDemoAction1(state, action) default: return state } } function handleDemoAction1 (state, action) { let counter = state.counter || 0 state = Object.assign({}, state, { counter: counter + 1 }) return state } // src/store/demoReducer_2.js import * as actionTypes from '.. /constants' export default function reducer (state = {}, action) { switch (action.type) { case actionTypes.DEMO_ACTION_2: return handleDemoAction2(state, action) default: return state } } function handleDemoAction2 (state, action) { let counter = state.counter || 0 state = Object.assign({}, state, { counter: counter + 1 }) return state }Copy the code

4. Test results

When clicking Button 2, CounterView_2 shows the latest counter value, and CounterView_1 shows the same value as expected. However, console output shows that both CounterView_1 and CounterView_2 have been rerendered, contradicting expectations. Console screenshot is as follows:


5. Consider: Why does a change in irrelevant data in the Store cause a re-rendering of the interface?

(1) A brief review of redux knowledge

There are three core elements in REdux: Store, Reducer, and Action. Store, as the only data source of the application, is used to store the status data of the application at a certain time. Store is read-only and can only be changed by action, i.e. converting a store in state 1 to a store in state 2 through action. Reducer is used to define the details of state transitions and corresponds to actions. The UI of the application can listen for store changes through the SUBSCRIBE method provided by the Store. When a store in state 1 converts to a Store in state 2, all listeners registered with the SUBSCRIBE method will be called.

(2) Connect returns a HOC that listens for store changes

For HOC, read the React documentation: Higher Order Components. Here we’ll focus on the details of CONNECT.

Note: All of the react-Redux source code snippets in this article are from [email protected] and will not be repeated below.

React-redux: react-redux

export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { // ... return function wrapWithConnect(WrappedComponent) { // ... class Connect extends Component { // ... trySubscribe() { if (shouldSubscribe && ! this.unsubscribe) { this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) this.handleChange() } } tryUnsubscribe() { if (this.unsubscribe) { this.unsubscribe() this.unsubscribe = null } } componentDidMount() { this.trySubscribe() } componentWillUnmount() { this.tryUnsubscribe() // ... } handleChange() { if (! this.unsubscribe) { return } const storeState = this.store.getState() const prevStoreState = this.state.storeState if (pure && prevStoreState === storeState) { return } if (pure && ! this.doStatePropsDependOnOwnProps) { // ... } this.hasStoreStateChanged = true this.setState({ storeState }) } // ... } render() { const { // ... renderedElement } = this // ... if (! haveMergedPropsChanged && renderedElement) { return renderedElement } if (withRef) { this.renderedElement = createElement(WrappedComponent, { ... this.mergedProps, ref: 'wrappedInstance' }) } else { this.renderedElement = createElement(WrappedComponent, this.mergedProps ) } return this.renderedElement } // ... return hoistStatics(Connect, WrappedComponent) } // ... }Copy the code

In componentDidMount, Connect HOC listens for store changes through the subscribe method of store. In the handleChange callback, it first determines whether store changes. If store changes, Rerender Connect HOC using the setState method.

Note that in the Render method of Connect HOC, when haveMergedPropsChanged = false and renderedElement is present, the existing renderedElement is returned, The WrappedComponent will not be re-rendered; Otherwise a new renderedElement will be created and returned, causing the WrappedComponent to re-render.

(3) Why haveMergedPropsChanged?

The connect method accepts four parameters: mapStateToProps, mapDispatchToProps, mergeProps, and options. The most familiar and used parameters are the first two. When we need to get a reference to WrappedComponent, We will use the withRef property in the fourth argument, options, {withRef: true}. With regard to the third parameter, mergeProps is less exposed (at least in the projects I’ve worked on), and before I can explain mergeProps, it’s important to know what parts of the data Connect HOC is dealing with.

Connect HOC handles three types of data: stateProps, dispatchProps, and ownProps. Where stateProps is calculated by mapStateToProps, dispatchProps is calculated by mapDispatchToProps, and ownProps is transmitted to Connect HOC by the parent control.

According to the react-Redux documentation, the definition of mergeProps is:

[mergeProps(stateProps, dispatchProps, ownProps): props] (Function)
Copy the code

MergeProps takes the stateProps, dispatchProps, and ownProps as parameters and returns a props that is ultimately passed to the WrappedComponent. If mergeProps is not specified for connect, object.assign ({}, ownProps, stateProps, dispatchProps) is used by default. If the default value is used, if there is an attribute of the same name in the stateProps and ownProps, the value in the stateProps overrides the value in the ownProps.

Here are a few usage scenarios for mergeProps:

/* * When there are too many internal properties of stateProps and dispatchProps (especially dispatchProps), * by default, mergeProps copies these properties in turn into the WrappedComponent props, * This causes the WrappedComponent props to be too large, which increases the debugging complexity. This implementation of * * mergeProps effectively avoids the above problems. */ function mergeProps (stateProps, dispatchProps, ownProps) { return { stateProps, dispatchProps, OwnProps}} /* * Now suppose you have a list of articles, each of which has id, abstract, Creator, etc. * information. For some reason, this information only exists in the State of the Component, not in the Store (* calls the interface directly in the Component and saves the result as setState). When you enter the details of an article, * data that already exists in the client should be displayed immediately, such as Creator. To do this, you need to pass the data through ownProps *. * * On the article details page, the article details are requested and stored in the Store, and the article details are retrieved from the Store via mapStateToProps *. When the interface returns, it usually uses some default value in place of the real value. Therefore, * stateProps. Creator may be the default value {id: undefined, avatar: undefined,... }. By default, the properties of mergeProps with the same name are overridden by the values of the stateProps, and creator is the uninitialized default state that is finally obtained from the WrappedComponent props. The creator information cannot be displayed immediately after * enters the article details, even if the relevant data already exists in the article list. * * Using mergeProps solves this problem to some extent, as shown in the sample code below. */ function mergeProps (stateProps, dispatchProps, ownProps) { if (stateProps.creator && ownProps.creator) { if (! stateProps.creator.id) { delete stateProps.creator } } return Object.assign({}, ownProps, stateProps, * * Completely discards stateProps, dispatchProps, and ownProps, Function mergeProps (stateProps, dispatchProps, ownProps) {return {a: 1, b: 2,... }}Copy the code

(4) When will WrappedComponent be re-rendered?

As you can see from the Connect HOC render method snippet above, WrappedComponent rerenders when haveMergedPropsChanged = true or renderedElement is not present. RenderedElement is a cache of the result of the last call to createElement, and createElement will always have a value (regardless of errors), except for the first call to Connect HOC’s Render method. So whether the WrappedComponent rerenders is determined by the value of the haveMergedPropsChanged, or whether the mergedProps changes. MergedProps changed, WrappedComponent rerendered; Otherwise, do not re-render.

Here’s some of the logic in Connect HOC’s render method:

render () { const { // ... renderedElement } = this // ... let haveStatePropsChanged = false let haveDispatchPropsChanged = false haveStatePropsChanged = this.updateStatePropsIfNeeded() haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded() let haveMergedPropsChanged  = true if ( haveStatePropsChanged || haveDispatchPropsChanged || haveOwnPropsChanged ) { haveMergedPropsChanged = this.updateMergedPropsIfNeeded() } else { haveMergedPropsChanged = false } if (! haveMergedPropsChanged && renderedElement) { return renderedElement } if (withRef) { // ... } else { // ... }}Copy the code

In this code, when one of the stateProps, dispatchProps, and ownProps changes, it checks whether mergedProps has changed.

updateMergedPropsIfNeeded() {
  const nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props)
  if (this.mergedProps && checkMergedEquals && shallowEqual(nextMergedProps, this.mergedProps)) {
    return false
  }

  this.mergedProps = nextMergedProps
  return true
}
Copy the code

If mergeProps is the default value, a simple derivation shows that when one of the stateProps, dispatchProps, and ownProps changes, mergedProps also changes, resulting in a re-rendering of the WrappedComponent.

(5) How to determine whether ownProps are changed?

In the Connect HOC, whether ownProps change in componentWillReceiveProps, code is as follows:

componentWillReceiveProps(nextProps) { if (! pure || ! shallowEqual(nextProps, this.props)) { this.haveOwnPropsChanged = true } }Copy the code

Pure is an optional configuration. Its value is taken from the connect fourth parameter options, which defaults to true. (Read the React-Redux source code for more details on pure, which is not discussed here.)

const { pure = true, withRef = false } = options
Copy the code

If pure is the default, ownProps changes when the parent control passes to Connect HOC props changes.

(6) How to determine whether the stateProps and dispatchProps are changed? StateProps, for example

Connect HOC determines if the stateProps have changed by calling the updateStatePropsIfNeeded method in render:

updateStatePropsIfNeeded() { const nextStateProps = this.computeStateProps(this.store, this.props) if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) { return false } this.stateProps = nextStateProps return true } computeStateProps(store, props) { if (! this.finalMapStateToProps) { return this.configureFinalMapState(store, props) } const state = store.getState() const stateProps = this.doStatePropsDependOnOwnProps ? this.finalMapStateToProps(state, props) : this.finalMapStateToProps(state) if (process.env.NODE_ENV ! == 'production') { checkStateShape(stateProps, 'mapStateToProps') } return stateProps }Copy the code

Connect HOC determines whether the stateProps is changed by shallowEqual of this. StateProps and nextStateProps. DispatchProps is similar to stateProps and will not be discussed again.

(7) Answer our question: why does the change of irrelevant data in store cause the re-rendering of the interface?

Let’s look at the implementation of mapStateToProps in the example above:

const mapStateToProps = (state) => { let demo_1 = Object.assign({}, state.demo_1) if (! demo_1.counter) { demo_1.counter = 0 } return { demoData: demo_1 } }Copy the code

Let demo_1 = object.assign ({}, state.demo_1). Each time mapStateToProps is called, a new Object instance is created and assigned to demo_1. If mapStateToProps is called twice in a row, then:

let thisStateProps = mapStateToProps(state) let nextStateProps = mapStateToProps(state) assert(thisStateProps.demoData ! == nextStateProps.demoData)Copy the code

In updateStatePropsIfNeeded, shallowEqual nextStateProps and thisStateProps, because thisStateProps. DemoData! == nextStateProps. DemoData, updateStatePropsIfNeeded will return true, stateProps changed.

To sum up, when Button 2 is clicked, DEMO_ACTION_2 is dispatched and the store changes. CounterView1’s Connect HOC handleChange callback detects store changes and rerenders Connect HOC via setState. In the Render method of Connect HOC, because this.stateprops. DemoData! = = nextStateProps demoData, enclosing updateStatePropsIfNeeded returns true, said stateProps changed. In the fourth dot, ‘When will WrappedComponent be rerendered? When the stateProps changes, CounterView1 will be rerendered.

6. Think again: How do we write mapStateToProps?

When Button 2 is clicked, CounterView1 is re-rendered because each time mapStateToProps is called, a new Object instance is created and assigned to Demo_1, This caused shallowEqual in updateStatePropsIfNeeded to fail, stateProps to change, and CounterView1 to be rerendered.

(1) Optimize the above writing method of mapStateToProps

Take a look at the following code:

let obj = { p1: { a: 1 }, p2: { b: 2 } }
let obj2 = Object.assign({}, obj, { p3: { c: 3 } })
assert(obj.p1 === obj2.p1)
Copy the code

ThisStore! ThisStore! Thisstore. demo_1 === nextstore. demo_1, the object pointed to by demo_1 in store has not changed. In the implementation of mapStateToProps above, the biggest problem is that every time mapStateToProps is called, stateProps. DemoData points to a new object. What if I assigned store.demo_1 directly to stateProps. DemoData? The modified code is as follows:

const mapStateToProps = (state) => { let demo_1 = state.demo_1 if (! demo_1.counter) { demo_1 = { counter: 0 } } return { demoData: demo_1 } }Copy the code

After executing the modified code, the console output looks like this:


From the console log, when Button 1 is clicked for the first time, both CounterView1 and CounterView2 are rerendered; When Button 2 is subsequently clicked, only CounterView2 is rerendered. Why?

When Button 1 is clicked for the first time, store.demo_2 is the default value for initialization, so if (! Counter) {demo_2 = {counter: 0}} logic branch, in mapStateToProps, we created a new default each time. Optimize mapStateToProps again as follows:

const DefaultDemoData = { counter: 0 } const mapStateToProps = (state) => { let demo_1 = state.demo_1 if (! demo_1.counter) { demo_1 = DefaultDemoData } return { demoData: demo_1 } }Copy the code

After executing this optimization code, the console output looks like this:


When Button 1 or Button 2 is clicked, only the corresponding CounterView is re-rendered.

(2) Try to generalize some basic principles

  • Instead of building new objects in mapStateToProps, use the corresponding objects in store
  • Provide global defaults so that the returned defaults point to the same object each time

A common error when using immutable is:

let immutableRecord = state['reducer_key'].immutableRecord || DefaultImmutableRecord
immutableRecord = immutableRecord.toJS()
Copy the code

Each time the toJS() method is called, a new object is generated, causing the stateProps to change and the interface to be rerendered.

(3) We cannot avoid building new objects in mapStateToProps

We now assume the following store structure, which is immutable for convenience:

new Map({
  feedIdList_1: new List([id_1, id_2, id_3]),
  feedIdList_2: new List([id_1, id_4, id_5]),
  feedById: new Map({
    id_1: FeedRecord_1,
    ...
    id_5: FeedRecord_5
  })
})
Copy the code

To render a list of feeds on the interface, such as feedIdList_1, we connect the list of feeds to the Store and then build an array of feeds in mapStateToProps, one for each element in the array. The code for mapStateToProps is as follows:

const DefaultList = new List() const mapStateToProps = (state, OwnProps) = > {let feedIdList = state [' reducer_key] feedIdList_1 | | DefaultList / / because of the react can't render immutable list, FeedIdList = feedidlist.tojs () let feedList = feedidlist.map ((feedId) => {return state['reducer_key'].getIn(['feedById', feedId]) }) return { feedList: feedList, // other data } }Copy the code

In the implementation of mapStateToProps, each time I call mapStateToProps, I create a new feedList object. As we can see from the above, even though the feedRecords for feedIdList_1 and ID_1, ID_2, and ID_3 are not changed, Changes in other parts of the store also cause the feed list to be re-rendered.

Note: When rendering a list on the interface, we usually connect the list to the Store rather than each element of the list to the Store. For more information on where to use connect, seeredux/issues/815.redux/issues/1255andAt what nesting level should components read entities from Stores in Flux?

7. Think again: How can we avoid rerendering caused by changes to irrelevant data in the Store?

(1) shouldComponentUpdate

ShouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldComponentUpdate shouldProps ShallowEqual or some other method) to determine whether the interface is re-rendered.

Although shouldComponentUpdate can be used to avoid re-rendering due to changes in irrelevant data in the Store, every time the Store changes, all mapStateToProps are re-executed, which can cause some performance issues.

(2) An extreme example

const DefaultDemoData = { counter: 0 } const mapStateToProps = (state, ownProps) => { let demo_1 = state.demo_1 if (! demo_1.counter) { demo_1 = DefaultDemoData } // tons of calculation here, for example: let counter = demo_1.counter for (let i = 0, i < 1000000000000; i++) { counter *= i } demo_1.counter = counter return { demoData: demo_1 } }Copy the code

In this extreme example, mapStateToProps has a very time-consuming calculation. While shouldComponentUpdate can effectively avoid re-rendering, how can we effectively avoid this complex calculation?

(3) A new idea

Redux takes full advantage of the idea of pure functions, and our mapStateToProps is itself a pure function. The characteristic of a pure function is that when the input is constant, the same pure function is executed many times and the result is the same. Since demo_1.counter returns the same value every time we execute this time-consuming operation without changing its value, can we cache the result and read the cached value when demo_1.counter does not change? Modify the above code as follows:

const DefaultDemoData = { counter: 0 } const tonsOfCalculation = (counter) => { let lastCounter, lastResult return () => { if (lastCounter && lastResult && lastCounter === counter) { return lastResult } lastCounter = counter for (let i = 0, i < 1000000000000; i++) { counter *= i } lastResult = counter return counter } } const mapStateToProps = (state, ownProps) => { let demo_1 = state.demo_1 if (! demo_1.counter) { demo_1 = DefaultDemoData } demo_1.counter = tonsOfCalculation(demo_1.counter)() return { demoData: demo_1 } }Copy the code

In tonsOfCalculation, we judge whether the input changes by recording the value of the incoming counter and comparing it with the current incoming counter. When the counter changes, we recalculate and cache the result. When counter is unchanged and the cache value exists, the cache value is read directly, effectively avoiding unnecessary time-consuming calculations.

(4) Apply the idea of cache to mapStateToProps

With the idea of caching, we need to achieve two goals:

  • When the dependent data in the store has not changed, the last built object is read directly from the cache, avoiding re-rendering. Example: When the FeedRecord of feedIdList_1 and ID_1, ID_2, and ID_3 is unchanged, the feedList is read directly from the cache to avoid building new objects
  • Avoid time-consuming operations that might exist in mapStateToProps. Example: When counter does not change, the cache value is read directly

Dependent data in store: That is, the data that the mapStateToProps/selector needs to read from the store to achieve its specific calculation purpose. For example, our feedList_1 depends on feedIdList_1, feedByID.id_1, FeedById. Id_2 and feedById. Id_3

The code to modify the above feed list example is as follows:

const LRUMap = require('lru_map').LRUMap const lruMap = new LRUMap(500) const DefaultList = new List() const DefaultFeed  = new FeedRecord() const mapStateToProps = (state, ownProps) => { const hash = 'mapStateToProps' let feedIdList = memoizeFeedIdListByKey(state, lruMap, 'feedIdList_1') let hasChanged = feedIdList.hasChanged if (! hasChanged) { hasChanged = feedIdList.result.some((feedId) => { return memoizeFeedById(state, lruMap, feed).hasChanged }) } if (! hasChanged && lruMap.has(hash)) { return lruMap.get(hash) } let feedIds = feedIdList.result.toJS() let feedList = feedIds((feedId) => { return memoizeFeedById(state, lruMap, feed).result }) // do some other time consuming calculations here let result = { feedList: feedList, // other data } lruMap.set(hash, result) return result } function memoizeFeedIdListByKey (state, lruMap, idListKey) { const hash = `hasFeedIdListChanged:${idListKey}` let cached = lruMap.get(hash) let feedIds = state['reducerKey'][idListKey] let hasChanged = feedIds && cached ! == feedIds if (hasChanged) { lruMap.set(hash, feedIds) } return { hasChanged: hasChanged, result: feedIds || DefaultList } } function memoizeFeedById (state, lruMap, feedId) { const hash = `hasFeedChanged:${feedId}` let cached = lruMap.get(hash) let feed = state['reducer_key'].getIn(['feedById', feedId]) let hasChanged = feed && cached ! == feed if (hasChanged) { lruMap.set(hash, feed) } return { hasChanged: hasChanged, result: feed || DefaultFeed } }Copy the code

The above code first detects whether the dependent data has changed (feedIdList_1 and the FeedRecord corresponding to ID_1, ID_2, ID_3). If it has not changed and the cache exists, the cached data will be returned directly without re-rendering the interface. If changes are made, the cache is recalculated and set, and the interface is re-rendered.

(5) Introduce a new library: ResELECT

Reselect takes advantage of this idea to avoid double-counting of mapStateToProps and unnecessary rendering of the interface by detecting whether the dependent data in the Store has changed. We’ll focus on resELECT usage scenarios and their limitations below.

Should we use shouldComponentUpdate in WrappedComponent?

Should we use shouldComponentUpdate in WrappedComponent if we can use caching to avoid unnecessary interface rendering in mapStateToProps? As mentioned above, Connect HOC mainly deals with three types of data stateProps, dispatchProps and ownProps. The idea of caching can effectively avoid unnecessary rendering caused by stateProps and dispatchProps. So what happens when ownProps changes? Look at the following example:

// src/app.js

render () {
  return (
    <div>
      <CounterView1 otherProps={{ a: 1 }}>
    </div>
  )
}
Copy the code

In CounterView1 componentWillReceiveProps, you will find nextProps. OtherProps! == this.props. OtherProps, which causes CounterView1 to be re-rendered. This is because every time SRC /app.js is re-rendered, a new otherProps object is built and passed to CounterView1. In this case, we can use shouldComponentUpdate to avoid such unnecessary rendering caused by ownProps.

ShouldComponentUpdate has many other applications, but this is not the scope of this article, so I won’t list them all.

Second, reselect

Reselect is designed based on the following three principles:

  • Selectors can be used to evaluate derived data, allowing Redux to store only the smallest possible state
  • Selectors are efficient; a selector is recalculated only when the parameters passed to it change
  • Selectors can be composed, and they can be used as input to other selectors

In the second principle above, in order to make selectors efficient, you need to use the caching idea mentioned earlier.

Note: The above three principles have been translated from the ResELECT documentation, please see for more detailshere

1. How to use ResELECT

Here is an example of how to use resELECT as an immutable store:

{
  feed: new Map({
    feedIdList_1: new List([id_1, id_2, id_3]),
    feedIdList_2: new List([id_1, id_4, id_5]),
    feedById: new Map({
      id_1: FeedRecord_1,
      ...
      id_5: FeedRecord_5
    })
  }),
  ...
}
Copy the code

Here is part of the code implementation:

import { createSelector } from 'reselect'

const getFeedById = state => state['feed'].get('feedById')
const getFeedIds = (state, idListKey) => state['feed'].get(idListKey)

const feedListSelectorCb = (feedIds, feedMap) => {
  feedIds = feedIds.toJS ? feedIds.toJS() : feedIds
  let feedList = feedIds.map((feedId) => {
    return feedMap.get(feedId)
  })
}
const feedListSelector = createSelector(getFeedIds, getFeedById, feedListSelectorCb)

const mapStateToProps = (state, ownProps) => {
  const idListKey = 'feedIdList_1'
  let feedList = feedListSelector(state, idListKey)
  return {
    feedList: feedList,
    // other data
  }
}
Copy the code

Here, we create the feedListSelector using the createSelector method provided by ResELECT, and calculate the feedList by calling the feedListSelector in mapStateToProps. The relevant dependency data for feedListSelector are feedById and feedIdList_1. When either of these changes, the resELECT internal mechanism determines the change and calls feedListSelectorCb to recalculate the new feedList. We’ll discuss resELECT in more detail later.

This code is much cleaner than the previous feed list implemented with lruMap, but there is a problem with this code.

(1) The above code has the problem of re-rendering the interface due to the change of irrelevant data in store

The feedListSelector dependencies above are feedById and feedIdList_1. By looking at the store structure, we can see that there is data in feedById that is not related to feedList_1. That is, in order to calculate feedList_1, feedListSelector relies on data unrelated to feedList_1, namely FeedRecord_4 and FeedRecord_5. When FeedRecord_5 changes, feedById changes, causing feedListSelectorCb to be called again and a new feedList will be returned. As we know from the discussion above, when new objects are created in mapStateToProps, this results in a re-rendering of the interface.

In both feedList_1 before and after the change of FeedRecord_5, although no element in feedList_1 has changed, feedList_1 itself has changed (two different objects), resulting in interface rendering, This is a typical example of an interface rendering caused by irrelevant data changes in the Store.

(2) A more complicated example

In practical applications, a feed also has the creator attribute, and Creator, as a user, may also have information such as the organization to which it belongs. The structure of some stores is as follows:

{ feed: { feedIdList_1: new List([feedId_1, feedId_2, feedId_3]), feedIdList_2: new List([feedId_1, feedId_4, feedId_5]), feedById: new Map({ feedId_1: new FeedRecord({ id: feedId_1, creator: userId_1, ... }),... feedId_5: FeedRecord_5 }) }, user: { userIdList_1: new List([userId_2, userId_3, userId_4]) userById: new Map({ userId_1: new UserRecord({ id: userId_1, organization: organId_1, ... }),... userId_3: UserRecord_3 }) }, organization: { organById: new Map({ organId_1: new OrganRecord({ id: organId_1, name: 'Facebook Inc.', ... }),... }}})Copy the code

The above store is mainly composed of feed, user and organization, which are updated internal data by different reducer respectively. When rendering feedList_1, each feed needs to show information about the creator and the organization to which the creator belongs. To do this, our feedListSelector needs to make the following changes.

import { createSelector } from 'reselect' const getFeedById = state => state['feed'].get('feedById') const getUserById =  state => state['user'].get('userById') const getOrganById = state => state['organization'].get('organById') const getFeedIds = (state, idListKey) => state['feed'].get(idListKey) const feedListSelectorCb = (feedIds, feedMap, userMap, organMap) => { feedIds = feedIds.toJS ? feedIds.toJS() : feedIds let feedList = feedIds.map((feedId) => { let feed = feedMap.get(feedId) let creator = userMap.get(feed.creator) let organization = organMap.get(creator.organization) feed = feed.set('creator', creator) feed = feed.setIn(['creator', 'organization'], organization) return feed }) } const feedListSelector = createSelector( getFeedIds, getFeedById, getUserById, getOrganById feedListSelectorCb ) const mapStateToProps = (state, ownProps) => { const idListKey = 'feedIdList_1' let feedList = feedListSelector(state, idListKey) return { feedList: feedList, // other data } }Copy the code

In the code above, the relative dependencies for feedListSelector are feedIdList_1, feedById, userById, and organById. There are two more dependencies, userById and organById, than in the previous simple feed list example. Here’s an interesting thing: when we request userList_1 data from the server and store it in the store, it causes feedList_1 to be re-rendered because the userById has changed. This is not what we expected from a performance perspective.

(3) Can the above problems be solved by changing the structure of store?

The main reason for the above problem is that the feedListSelector dependency data feedById, userById, etc. contains data that is not related to feedList_1. So can we store related data together, so that feedListSelector doesn’t rely on irrelevant data.

As an example of a simple list of feeds mentioned above, the modified store structure looks like this:

{
  feed: new Map({
    feedList_1: new Map({
      idList: new List([id_1, id_2, id_3]),
      feedById: new Map({
        id_1: FeedRecord_1,
        id_2: FeedRecord_2,
        id_3: FeedRecord_3
      })
    }),
    feedList_2: new Map({
      idList: new List([id_1, id_4, id_5]),
      feedById: new Map({
        id_1: FeedRecord_1,
        id_4: FeedRecord_4,
        id_5: FeedRecord_5
      })
    })
  }),
  ...
}
Copy the code

In this case, each feedList has its own idList and feedById. When rendering feedList_1, the feedListSelector needs to rely on the feedList_1 data.

import { createSelector } from 'reselect'

const getFeedList = (state, feedListKey) => state['feed'].get(feedListKey)

const feedListSelectorCb = (feedListMap) => {
  let feedMap = feedListMap.get('feedById')
  let feedIds = feedListMap.get('idList').toJS()
  let feedList = feedIds.map((feedId) => {
    return feedMap.get(feedId)
  })
}
const feedListSelector = createSelector(getFeedList, feedListSelectorCb)

const mapStateToProps = (state, ownProps) => {
  const feedListKey = 'feedList_1'
  let feedList = feedListSelector(state, feedListKey)
  return {
    feedList: feedList,
    // other data
  }
}
Copy the code

Since our feedListSelector no longer relies on extraneous data, changes to the corresponding FeedRecord of ID_4 or ID_5 will no longer cause feedList_1 to re-render. However, this store structure has the following problems:

  • Duplicate data exists in store. The FeedRecord corresponding to ID_1 exists in both feedList_1 and feedList_2, which may cause large data redundancy
  • When the FeedRecord corresponding to ID_1 in feedList_2 changes, feedList_1 will not re-render, i.e. the change of relevant data does not cause interface rendering problems

How to define our Data Model: ‘Normalized Data Model’ vs ‘Embedded Data Model’

In analyzing a simple list of feeds, I mentioned two data models. The first is Normalized Data Model, which, when used with ResELECT, causes unnecessary interface rendering due to changes to irrelevant Data in the store. The second is the Embedded Data Model, which, when used with ResELECT, has the problem that changes in store Data do not cause interface rendering. So how do we define the data model in the Store?

(1) Introduce two concepts: Store model and display model, as well as some general practices

With Redux, there are two main parts of the data we need to process: the store part, which represents the global state of the application, and the derived data, which is calculated to render a specific interface. These two parts of data generally adopt different data models:

  • Store Model: a Normalized Data model is used to store Data in a store
  • Display Model: Data model required for interface rendering. Embedded Data model is generally used

We use mapStateToProps and Selector to convert Normalized data in the store to Embedded data needed by the interface.

(2) Briefly analyze the advantages and disadvantages of Normalized and Embedded data models

A Normalized Data Model example:

{ feedList: [feedId_1, feedId_2, feedId_3, ...] , feedById: { feedId_1: { id: feedId_1, title: 'Feed Title', content: 'Feed Content', creator: userId_1 }, feedId_2: {... },... }, userList: [userId_1, userId_2, ...] , userById: { userId_1: { id: userId_1 , nickname: 'nickname', avatar: 'avatar.png', ... },... }}Copy the code

An example of the Embedded Data Model:

{ feedList: [ { id: feedId_1, title: 'Feed Title', content: 'Feed Content', creator: { id: userId_1 , nickname: 'nickname', avatar: 'avatar.png', ... }, ... }, ... ] , userList: [ { id: userId_1 , nickname: 'nickname', avatar: 'avatar.png', ... }, ... ] }Copy the code

Normalized Data Model:

  • Advantages: Using ids to associate data, data storage is flat, no data redundancy, high data consistency, and easy update operation
  • Disadvantages: To render relevant data, de-normalized data needs to be converted to Embedded data suitable for UI rendering. This process may be time-consuming when the amount of data is large. It can also cause unnecessary interface rendering due to the problem of creating new objects mentioned earlier

Embedded Data Model:

  • Advantages: High efficiency of rendering data
  • Disadvantages: Data is nested, there is a large amount of data redundancy, and complex (and sometimes inefficient) data update logic is required to ensure data consistency. For example, when the Avatar of userId_1 changes, in the Normalized Data Model structure, you only need to find the UserRecord in userById and update the Avatar value. In Embedded Data Model, feedList and userList need to be traversed respectively to find the corresponding UserRecord and then update.

For more information about Normalized Data Model and Embedded Data Model, see Data Model Design. Git redux and React

  • How to handle case where the store “model” differs from the “display” model
  • Memoizing Hierarchial Selectors
  • Performance Issue with Hierarchical Structure
  • TreeView.js
  • normalizr

3. Analysis of resELECT’s internal mechanism

Note: the source code snippets for resELECT in this section are from [email protected] and will not be highlighted in subsequent sections

Reselect has fewer sources, the most important of which are the createSelectorCreator and defaultMemoize functions:

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) { let lastArgs = null let lastResult = null // position 1 return function () { if (! areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) { // position 4 lastResult = func.apply(null, arguments) } lastArgs = arguments return lastResult } } export function createSelectorCreator(memoize, ... memoizeOptions) { return (... funcs) => { let recomputations = 0 const resultFunc = funcs.pop() const dependencies = getDependencies(funcs) const memoizedResultFunc = memoize( // position 3 function () { recomputations++ return resultFunc.apply(null, arguments) }, . memoizeOptions ) const selector = defaultMemoize( // position 2 function () { const params = [] const length = dependencies.length for (let i = 0; i < length; i++) { params.push(dependencies[i].apply(null, arguments)) } // position 5 return memoizedResultFunc.apply(null, params) } ) selector.resultFunc = resultFunc selector.recomputations = () => recomputations selector.resetRecomputations  = () => recomputations = 0 return selector } } export const createSelector = createSelectorCreator(defaultMemoize)Copy the code

The following looks at the resELECT source code in conjunction with the simple feed list example mentioned above.

First we create a feedListSelector using the createSelector method provided by ResELECT:

const feedListSelector = createSelector(getFeedIds, getFeedById, feedListSelectorCb)
Copy the code

In createSelectorCreator source code dependencies = [getFeedIds, getFeedById], resultFunc = feedListSelectorCb. FeedListSelector is equal to the internal function returned by defaultMemoize, that is, the internal function at Position 3.

Calculate the feedList in mapStateToProps:

let feedList = feedListSelector(state, idListKey)
Copy the code

We call feedListSelector and pass in two arguments, state and idListKey. We said that the feedListSelector points to an internal function at Position 3. Inside that function, we first determine whether the current argument passed is the same as the last one. If the state and idListKey have not changed, return the result of the last calculation; Otherwise, execute the code at Position 4 and recalculate the feedList.

Func at position 4 points to the inner function at position 2, and since func.apply(null, arguments), all the arguments we pass to feedListSelector are passed to the inner function at Position 2. Inside the function at Position 2, the dependency data is first calculated by executing getFeedIds and getFeedById in sequence. Apply (null, arguments); getFeedIds; getFeedById

At position 5, all relevant dependency data is passed as parameters to memoizedResultFunc in turn. Since createSelector uses defaultMemoize, memoizedResultFunc also points to the internal function at Position 1. Inside the function at position 1, it first determines whether the dependent data has changed. If the dependent data does not change, it directly returns the cached result. When the dependency data changes, the code at Position 4 is executed, where func in Position 4 points to resultFunc, or feedListSelectorCb.

(1) How does reselect access React Props?

As you can see from the above analysis, all arguments passed to feedListSelector are passed to getFeedById and getFeedIds in sequence. So we can pass React Props like this:

const mapStateToProps = (state, ownProps) => {
  let feedList = feedListSelector(state, ownProps)
  return {
    feedList: feedList,
    // other data
  }
}
Copy the code

(2) The default cache size for selectors created by createSelector is 1

As can be seen from the implementation of defaultMemoize above, it just uses THE JS closure to remember the parameters passed in last time and the result of calculation respectively with free variable. Since a variable can only store one value, its cache size is 1. This can cause some problems, see the following example:

let feedList_1 = feedListSelector(state, 'feedIdList_1') let feedList_2 = feedListSelector(state, 'feedIdList_2') let feedList_3 = feedListSelector(state, 'feedIdList_1') assert(feedList_1 ! == feedList_3)Copy the code

In the example above, we call feedListSelector several times and pass in a different feedIdListKey, provided by feedList_1! FeedListSelector does not cache as expected.

When the first call to feedListSelector evaluates feedList_1, defaultMemoize lastArgs = [state, ‘feedIdList_1’], when evaluating feedList_2, Arguments = [state, ‘feedIdList_2’], find that lastArgs does not equal arguments by shallowEqual comparison, and call the callback to calculate feedList_2. After the second call to feedListSelector, defaultMemoize lastArgs = [state, ‘feedIdList_2’]. The third call to feedListSelector evaluates feedList_3 with the same arguments as the first call, but since lastArgs does not equal the current arguments, the feedList is recalculated, so feedList_1! = = feedList_3.

For more discussion, see the ResELECT documentation: Sharing Selectors with Props Across Multiple Component Instances

Reselect and Normalized Data Model

As mentioned above, resELECT can cause unnecessary interface rendering when stored in a Normalized Data Model, especially in the case of complex feed lists.

In the simple feed list example, the main reason for this problem is that feedListSelector relies on feedById, which has data unrelated to feedList_1. Since the data related to feedList_1 is only feedIdList_1 and the corresponding FeedRecord of ID_1, ID_2 and ID_3. Can we just make the feedListSelector rely on that data alone? Like this pseudo-code:

const getFeedIds = (state, idListKey) => state['feed'].get(idListKey) const createGetFeed = (feedId) => (state) => state['feed'].getIn(['feedById',  feedId]) let feedListSelector = createSelector( createGetFeed(id_1), createGetFeed(id_2), createGetFeed(id_3), (feed_1, feed_2, feed_3) => [feed_1, feed_2, feed_3] )Copy the code

The answer is no. With ResELECT, we need to use createSelector to create a selector and determine the dependency data for that selector when we create it. When we create feedListSelector, we have no way of knowing which feeds are in the feedList to render, so this idea is not feasible. This is one of reselect’s limitations when it is used with Normalized Data Model. Here are some of ResELECT’s other limitations.

(1) Cache the calculation results of each feed

In our example of a complex feed list, feedListSelectorCb is implemented as follows:

const feedListSelectorCb = (feedIds, feedMap, userMap, organMap) => {
  feedIds = feedIds.toJS ? feedIds.toJS() : feedIds
  let feedList = feedIds.map((feedId) => {
    let feed = feedMap.get(feedId)
    let creator = userMap.get(feed.creator)
    let organization = organMap.get(creator.organization)

    feed = feed.set('creator', creator)
    feed = feed.setIn(['creator', 'organization'], organization)
    return feed
  })
}
Copy the code

In the above code, the calculation of the feed is complicated, and we want to cache the result of the calculation. Here we connect the entire list to the store:

const getFeedById = state => state['feed'].get('feedById')
const getUserById = state => state['user'].get('userById')
const getOrganById = state => state['organization'].get('organById')
const getFeedIds = (state, idListKey) => state['feed'].get(idListKey)
const getFeed = (state, feedId) => state['feed'].getIn(['feedById', feedId])

const feedSelectorCb = (feed, userMap, organMap) => {
  let creator = userMap.get(feed.creator)
  let organization = organMap.get(creator.organization)

  feed = feed.set('creator', creator)
  feed = feed.setIn(['creator', 'organization'], organization)
  return feed
}

const feedSelector = createSelector(
  getFeed, getUserById, getOrganById, feedSelectorCb
)

const feedListSelectorCb = (feedIds, feedMap, userMap, organMap) => {
  feedIds = feedIds.toJS ? feedIds.toJS() : feedIds
  let feedList = feedIds.map((feedId) => {
    return feedSelector(state, feedId)
  })
}

const feedListSelector = createSelector(
  getFeedIds, getFeedById, getUserById, getOrganById, feedListSelectorCb
)
Copy the code

The above code does not need to use feedMap, userMap, and organMap in feedListSelectorCb, but the feedListSelector must rely on the relevant data, otherwise, for example, when the FeedRecord corresponding to feedId_1 changes, FeedList_1 will not be re-rendered.

We also notice that when we call feedSelector in feedListSelectorCb, the feedSelector doesn’t get the state object. Recall the resELECT source code, which calls feedListSelectorCb and simply passes the results of calls to getFeedIds, getFeedById, getUserById, and getOrganById to feedListSelectorCb, There are no state objects in the feedListSelectorCb parameter table (although you can get state by global variables, or other means, this seems to defeat the purpose of ResELECT and is therefore not recommended).

In feedListSelectorCb, the feedSelector is called multiple times, passing in a different feedId each time. Due to reselect’s default cache size of 1, the feedSelector is recalculated every time it is called, which does not achieve the desired cache effect.

Connect separately for each feed in the feedList

In the example in the previous section, we connected the feedList to the Store and calculated in its mapStateToProps all the data needed to render the entire list. When we need to cache the results of each feed calculation, the approach in the previous section seems unlikely (welcome to the joke).

What if we connect each feed individually? Modified code:

// feedList.js
const getFeedIds = (state, idListKey) => state['feed'].get(idListKey)

const feedIdListSelector = createSelector(getFeedIds, (feedIds) => {
  return feedIds.toJS ? feedIds.toJS() : feedIds
})

const mapStateToProps = (state, ownProps) => {
  let idListKey = ownProps.idListKey || 'feedIdList_1'
  return feedIdListSelector(state, idListKey)
}

// feedCell.js
const getUserById = state => state['user'].get('userById')
const getOrganById = state => state['organization'].get('organById')
const getFeed = (state, feedId) => state['feed'].getIn(['feedById', feedId])

const feedSelectorCb = (feed, userMap, organMap) => {
  let creator = userMap.get(feed.creator)
  let organization = organMap.get(creator.organization)

  feed = feed.set('creator', creator)
  feed = feed.setIn(['creator', 'organization'], organization)
  return feed
}

const mapStateToProps = (state, ownProps) => {
  const feedSelector = createSelector(
    getFeed, getUserById, getOrganById, calculateFeedCb
  )

  return (state, ownProps) => {
    return feedSelector(state, ownProps.feedId)
  }
}
Copy the code

Note: this use of mapStateToProps in feedCell.js is intended to address the default resELECT cache size of 1, as described in feedCell.jsSharing Selectors with Props Across Multiple Component Instances. This will also be highlighted in the next chapter of Per-Component Memoization.

The above code serves our purpose of caching the results of each feed calculation. However, because feedSelector relies on userById and organById, there is still the problem of ‘unnecessary interface rendering due to changes in irrelevant data in the store’. In addition, because each feed is connected, this can cause too much connect across the application, which can lead to new problems. For more information on where to find connect, see At What Nesting level should components read entities from Stores in Flux.

(3) Reselect and Normalized Data Model

One of the features of Reselect is that it needs to determine the dependent data at selector creation time and determine whether the result of the selector needs to be recalculated by determining whether those dependent data have changed. A feature of a Normalized Data Model is to centralize stored Data (such as feedById) and reference the Data by ID. When reselect is used in conjunction with Normalized Data Model, it is difficult to avoid unnecessary interface rendering due to changes to irrelevant Data in the store. As mentioned above, connecting to the entire feedList or to each feedCell individually does not get rid of the userById and organById dependencies.

Personally, I don’t think Reselect works well with A Normalized Data Model (unless you can tolerate rerendering your feed list due to changes to irrelevant Data in userById or organById). If a Normalized Data Model is used, how do we cache the results of the selectors?

(4) Custom memoizeSelector

Reselect is a concrete example of memoizeSelector that does two main things:

  1. Determine whether the dependent data has changed
  2. Related dependent data unchanged, read cache; Correlation dependent data change, recalculate correlation results

Because ResELECT encapsulates the logic to determine whether dependent data has changed, it lacks the flexibility to deal with complex problems. We can customize this process to create our own memoizeSelector based on actual needs. In chapter 1, Section 7, section 4, “Applying the Idea of Caching to mapStateToProps,” we customized a memoizeSelector with lruMap. The code for mapStateToProps is as follows:

const mapStateToProps = (state, ownProps) => { const hash = 'mapStateToProps' let feedIdList = memoizeFeedIdListByKey(state, lruMap, 'feedIdList_1') let hasChanged = feedIdList.hasChanged if (! hasChanged) { hasChanged = feedIdList.result.some((feedId) => { return memoizeFeedById(state, lruMap, feed).hasChanged }) } if (! hasChanged && lruMap.has(hash)) { return lruMap.get(hash) } let feedIds = feedIdList.result.toJS() let feedList = feedIds((feedId) => { return memoizeFeedById(state, lruMap, feed).result }) // do some other time consuming calculations here let result = { feedList: feedList, // other data } lruMap.set(hash, result) return result }Copy the code

Unlike ResELECT, which is defined as determining the relevant dependency data and then determining whether the relevant dependency data has changed during execution. In this implementation, we determine the relative dependency data during the selector execution and determine whether the relative dependency data changes at the same time, which has greater flexibility and does not have the problem of ‘unnecessary interface rendering due to the change of irrelevant data in the store’.

In this implementation, we cannot avoid complete traversal of the dependent data, but this traversal is negligible compared to the re-rendering of the interface.

Third, Per – Component Memoization

1. How did the concept come about

Per-component Memoization is a new concept introduced by React-Redux in version 4.3.0. Prior to version 4.3.0, when using ResELECT to render multiple similar controls on the interface, such as feedList_1 and feedlist_2 at the same time, the only difference between feedList_1 and feedlist_2 is the idListKey in ownProps. As discussed above, reselect’s default cache size is 1, and rendering two FeedLists at the same time will result in the feedListSelector not being cached effectively. There is a lot of discussion on Git about this issue. Here are some of the discussions and materials I found: React-redux: react-redux: react-redux: react-redux: react-redux: react-redux: react-redux: react-redux: react-redux: react-redux: react-redux: React-redux /pull/185, react-redux/pull/279, resELECT /issues/79, reselect#accessing- react-functions-in-selectors

The results of these discussions can be briefly summarized as follows:

  • Previously, ComponentClass had a reference to a selectorInstance, so each componentInstance shared the same selectorInstance
  • What we want is for ComponentClass to have a reference to a SelectorClass, so each componentInstance can have its own selectorInstance

We can do this in two ways:

  • SelectorInstance can identify the componentInstance on which it depends, which, according to the discussion, doesn’t seem to work
  • Each componentInstance creates its own selectorInstance

Note: this is only a translation of the summary of the discussion, please click on the source of the summaryhere

2. mapStateToProps

We know that mapStateToProps can return an object, which is the calculated stateProps. But in some advanced scenarios, if you want more control over rendering performance, mapStateToProps can return a function that is passed to the specific Component instance as the mapStateToProps for normal use scenarios. Instead of sharing the same mapStateToProps, each Component instance will have its own mapStateToProps. We took advantage of this feature in chapter 2, Section 4, section 2, “Connect individually to each feed in the feedList.”

const mapStateToProps = (state, ownProps) => {
  const feedSelector = createSelector(
    getFeed, getUserById, getOrganById, calculateFeedCb
  )

  return (state, ownProps) => {
    return feedSelector(state, ownProps.feedId)
  }
}
Copy the code

This way, each feedCell has its own instances of mapStateToProps and feedSelector, so there are no caching problems caused by resELECT’s default cache size of 1.

3. Modify the implementation of the lruMap example above

In the lruMap example in Chapter 1, Section 7, section 4, we created a global lruMap and set the number of keys to 500. While sharing a global lruMap can be problematic in one way or another, we can now create an lruMap specific to a component instance, called per-Component memoization. The code is as follows:

const mapStateToProps = (state, ownProps) => {
  const LRUMap = require('lru_map').LRUMap
  const lruMap = new LRUMap(500)

  return (state, ownProps) => {
    const hash = 'mapStateToProps'
    let feedIdList = memoizeFeedIdListByKey(state, lruMap, 'feedIdList_1')
    let hasChanged = feedIdList.hasChanged
    if (!hasChanged) {
      hasChanged = feedIdList.result.some((feedId) => {
        return memoizeFeedById(state, lruMap, feed).hasChanged
      })
    }
    ...
  }
}
Copy the code

4. Cache interface data for a Component instance

For mobile apps, most of them choose to cache the data of some specific pages locally, so that they can show some data to users even when they open the App again in offline state. It can also hide the loading process of time-consuming interfaces from the user. With Redux, you can use the redux-persist library to cache data from the store to local storage, which is read and restored to the Store each time the App is opened.

The following problems can occur with redux-persist:

  • Redux-persist typically recovers store data with a Rehydrate event on App launch, but if the store is too large, recovery can be time-consuming, which affects App launch speed.
  • Each record in SQLite on the Android terminal has a maximum limit of 2MB. If the store data corresponding to a Reducer key is too large, errors may be reported by CRIED’t Read row 0, col 0 from CursorWindow, as shown here and here.
  • While you can control the size of cached data using Whitelist and Transforms, implementing transforms for specific page data can be difficult;

Per-component Memoization is a feature we can use to cache interface or store data for certain pages. Redux-persist is also used to store low-volume global data such as login status and configuration information.

The following pseudocode uses feed details as an example to show how per-component Memoization can be used to cache interface data for a page, with feedId in ownProps:

import { AsyncStorage } from 'react-native'
import { fetchFeedDetail } from 'path-to-action-folder'
import { dispatchFeedDetailApiResult } from 'some-where-else'

const mapStateToProps = (state, ownProps) => {
  const hash = `feedDetail:${ownProps.feedId}`
  let apiResult = AsyncStorage.getItem(hash)
  if (apiResult) {
    dispatchFeedDetailApiResult({ apiResult: apiResult })
  }

  return (state, ownProps) => {
    return { ... }
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    fetchDatas: (payload) => {
      return dispatch(fetchDetailWrapper(ownProps, payload))
    },
    ...
  }
}

function fetchDetailWrapper (ownProps, payload) {
  return (dispatch, getState) => {
    let fetchParams = { ... }
    return dispatch(fetchFeedDetail(fetchParams)).then((result) => {
      const hash = `feedDetail:${ownProps.feedId}`
      AsyncStorage.setItem(hash, JSON.stringify(result))
      return result
    })
  }
}

export connect(mapStateToProps, mapDispatchToProps)
Copy the code

In mapStateToProps, to detect whether there is a local interface data, if any, is in some way by dealing with the data and dispatchFeedDetailApiResult storage to store; When fetchDatas is called in the Component instance to fetch article details data, the retrieved interface data is cached locally. In contrast to Redux-Persist, this implementation restores data when a page is entered rather than when the App is launched, and can cache interface data for a particular page.

Four,

I’ve been around React/Redux for a while, and I’ve done some projects with them. This paper mainly reflects on the problems encountered in the actual project, and gives some possible solutions. It also covered some basic issues like where to use Connect and what data model to use in the Store. Hope to give you some help.

This is my first technical sharing article. I hope you enjoy it. Due to the limitations of personal experience and ability, I would like to ask readers to correct any possible problems or wrong views in this article.

Finally, welcome to our React/Redux website: 12km.com.