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
执行以下步骤(撇开所有内部复杂性):
- 调用
mapStateToProps
和 mapDispatchToProps
。
- 浅比较结果
props
- 重新呈现
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
}
}
}
更新: 另外,我看不出有任何理由使用工厂风格 makeThingsSelector
和 makeMapStateToProps
。为什么不直接使用类似的东西:
...
// 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
通过将 areStatesEqual
和 areOwnPropsEqual
谓词属性添加到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
!
基于我刚刚阅读并重新阅读的所有 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
执行以下步骤(撇开所有内部复杂性):
- 调用
mapStateToProps
和mapDispatchToProps
。 - 浅比较结果
props
- 重新呈现
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
}
}
}
更新: 另外,我看不出有任何理由使用工厂风格 makeThingsSelector
和 makeMapStateToProps
。为什么不直接使用类似的东西:
...
// 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
通过将 areStatesEqual
和 areOwnPropsEqual
谓词属性添加到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
!