Redux、Reselect 和 ImmutableJS 导致子组件不必要的渲染

Redux, Reselect and ImmutableJS causing unnecessary renders on child components

基于我刚刚阅读并重新阅读的所有 Redux 和 Reselect 文档,如果 getThing() returns 的不可变映射是不等于上一个

...

// Selector

import { createSelector } from 'reselect'

const getThing = (state, thingId) => {
    return state.entities.getIn(['things', thingId])
}

const makeThingSelector = () => {
    return createSelector(
        [getThing],
        (thing) => {
            return thing.toJS()
        }
    )
}

export default makeThingSelector

...

// Container

const makeMapStateToProps = () => {
    return (state, ownProps) => {
        const { thingId } = ownProps
        const things = select.makeThingsSelector()(state, thingId)

        return {
            hasNoThings: things.length === 0,
            things
        }
    }
}

const Container = connect(
    makeMapStateToProps,
    mapDispatchToProps
)(Component)
...

这适用,除非我有子 'smart' 组件。在这种情况下,当父组件触发渲染时,子组件容器中调用的选择器始终处理该值,而不管结果是否是新的。

我一直在尝试将 ImmutableJS API 封装在我的选择器中,但这意味着为了避免在每次父级更新时重新渲染这些嵌套组件,我必须在 shouldComponentUpdate 函数。这很昂贵,而且似乎不是一个不错的解决方案。

应用程序状态已规范化,因此状态树的更新部分不是子组件所依赖的状态部分的分层父级。

我是不是漏掉了什么关键的东西?

在每次 store 更新时 react-redux 执行以下步骤(撇开所有内部复杂性):

  1. 调用 mapStateToPropsmapDispatchToProps
  2. 浅比较结果props
  3. 重新呈现 Component 以防新的 props 与之前的不同。

这样 mapStateToProps 将在每次 store 更新时按设计调用。以下代码行也是如此:

...
const things = select.makeThingsSelector()(state, visitId)
...

如您所见,每次都会创建新的 reselect 选择器,以有效防止任何记忆(reselect 中没有全局状态,每个选择器都会发生记忆)。

您需要做的是更改您的代码,以便在每次调用 mapStateToProps:

时使用同一个选择器
const thingSelector = select.makeThingsSelector();

...

const makeMapStateToProps = () => {
    return (state, ownProps) => {
        const { visitId } = ownProps
        const things = thingSelector(state, visitId)

        return {
            hasNoThings: things.length === 0,
            things
        }
    }
}

更新: 另外,我看不出有任何理由使用工厂风格 makeThingsSelectormakeMapStateToProps。为什么不直接使用类似的东西:

...

// Selector

export default createSelector(
  [getThing],
  (thing) => thing.toJS()
);

...

// Container

const mapStateToProps = (state, ownProps) => {
  const { visitId } = ownProps
  const things = select.thingsSelector(state, visitId)
  return {
    hasNoThings: things.length === 0,
    things
  }
}

const Container = connect(
    mapStateToProps,
    mapDispatchToProps
)(Component)
...

由于此应用程序中的 redux 状态使用 ImmutableJS 数据结构,因此可能不需要 Reselect

首先,ImmutableJS 仅操作受更改操作影响的数据结构切片,因此对较大状态的所有更改可能不会影响传递给容器的切片。

其次,redux connect函数returns默认是纯容器,遇到相同的slice不会re-render。然而,mapStateToProps 将被调用,因为整个状态和可能的 ownProps 已经改变。

为了更好地控制,同一容器的呈现可以直接链接到状态的特定切片的更改,并且 ownProps 通过将 areStatesEqualareOwnPropsEqual 谓词属性添加到connect 函数的第四个参数(更广为人知的是 options 对象)。

const mapStateToProps = ({ entities }, { thingId }) => {
    const things = entities.getIn(['things', thingId]).toJS();
    return {
        hasNoThings: things.length === 0,
        things
   };
};

const Container = connect(
    mapStateToProps,
    mapDispatchToProps,
    undefined, {
        areOwnPropsEqual: (np, pp) => np.thingId === pp.thingId,
        areStatesEqual: (ns, ps) => ns.entities.get(‘things’).equals(
            ps.entities.get(‘things’)
        )
    }
)(Component);

如果这两个谓词都为真,不仅容器及其子容器不会 re-render,甚至不会调用 mapStateToProps