Blagoh
Blagoh

Reputation: 1235

Avoid re-rendering not possible - but docs say should be no need of shouldComponentUpdate

The redux docs state that when using redux with react, so using the connect from the react-redux package. That there should be no need for shouldComponentUpdate - http://redux.js.org/docs/basics/UsageWithReact.html#implementing-container-components

Redux library's connect() function, which provides many useful optimizations to prevent unnecessary re-renders. (One result of this is that you shouldn't have to worry about the React performance suggestion of implementing shouldComponentUpdate yourself.)

However my component is unnecesarily updated because I am returing a new array every time even though the contents of the array is the same. Here is my code:

const AssetManagerSmart = connect(
    function(state) {
        const { assets } = state;
        return {
            assetIds: assets.map( asset => asset.id )
        }
    }
)

See this assetIds, it is a map of an array from the redux state. This is causing my component to re-render. To simplify the code above, we can imagine assetIds is assigned to a new shallowly equal array everytime, like this:

const AssetManagerSmart = connect(
    function(state) {
        const { assets } = state;
        return {
            assetIds: ['hi']
        }
    }
)

How come the docs say I should not need shouldComponentUpdate when I am encountering the above? The above I see everyone doing, even in the "usage with react" section on official redux docs.

Upvotes: 2

Views: 558

Answers (1)

Ori Drori
Ori Drori

Reputation: 191946

When connect is in the default pure mode it performs several checks that aim to avoid invoking mapStateToProps, and if it was invoked, prevent rerender of the wrapped component if nothing changed.

To check if it should run mapStateToProps (and mergeProps) connects makes 2 equality checks:

  • areStatesEqual - checks if the entire state changed using strict equality. Whenever the store changes this will return false. However, you can override it, to check if a certain piece of the store changed. In your case you might want to check if assets have changed:

    (next, prev) => prev.assets === next.assets
    
  • areOwnPropsEqual - makes a shallow equality checks to see if the props supplied to the component returned from connect have changed. Since you don't use props, you can theoretically override this to always return true, however this will prevent rerenders on prop changes completely.

If mapStateToProps is invoked 2 checks are performed to see if it should rerender the wrapped component:

  • areStatePropsEqual - shallow equality between the current and previous results of mapStateToProps. This will check if prev { assetIds: [] } equals { assetIds: [] }, and since the assetsIds array changed will return false.

  • areMergedPropsEqual - shallow equality between the current and previous results of mergeProps - the combined ownProps, mapStateToProps, mapDispatchToProps.

So if your state changes or your props change, mapStateToProps will be called, and the assetIds will recalculate. You can override the the areStatesEqual and areOwnPropsEqual to whitelist only specific changes that should invoke mapStateToProps.

The result of mapStateToProps will be shallow checked vs. the previous result. You can override the check to actually see if the items in the assetIds have changed, but that may be expensive, and you would also need to the same in areMergedPropsEqual. An easier options is to use a memoized selector.

Memoized Selectors

A selector can take a piece of the store, and compute derived data. A memoized selector will return the same result (without performing the computation) as long as the relevant part of the state did not change. Since the result is the same array (not a new array with similar values), this will pass the areStatePropsEqual and areMergedPropsEqual checks (unless something else changed).

Reselect is a library that helps you in creating memoized selectors.

This is how you can create a memoized selector for the assets' ids (not tested):

import { createSelector } from 'reselect'

const getAssets = ({ assets }) => assets; /** simple not memoized selector **/

const getAssetIds = createSelector( /** memoized selector **/
    getAssets,
    (assets) => assets.map(asset => asset.id)
);

Upvotes: 2

Related Questions