还请帮我标题并更好地表达问题。
我在react / redux架构中遇到了一个问题,我找不到很多例子。
我的应用程序依赖于不变性,引用相等性和PureRenderMixin
性能。但我发现很难用这种架构创建更通用和可重用的组件。
假设我有这样的通用ListView
:
const ListView = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
items: arrayOf(shape({
icon: string.isRequired,
title: string.isRequired,
description: string.isRequired,
})).isRequired,
},
render() {
return (
<ul>
{this.props.items.map((item, idx) => (
<li key={idx}>
<img src={item.icon} />
<h3>{item.title}</h3>
<p>{item.description}</p>
</li>
)}
</ul>
);
},
});
然后我想使用列表视图来显示redux商店中的用户。他们有这种结构:
const usersList = [
{ name: 'John Appleseed', age: 18, avatar: 'generic-user.png' },
{ name: 'Kate Example', age: 32, avatar: 'kate.png' },
];
到目前为止,这是我的应用程序中非常普通的场景。我通常把它渲染成:
<ListView items={usersList.map(user => ({
title: user.name,
icon: user.avatar,
description: `Age: ${user.age}`,
}))} />
问题是map
打破了引用相等性。的结果
usersList.map(...)
将始终是新的列表对象。因此,即使usersList
自上一次渲染后未发生更改,ListView
也无法知道它仍然具有相同的items
- 列表。
我可以看到三种解决方案,但我不太喜欢它们。
解决方案1:将transformItem
- 函数作为参数传递,让items
成为原始对象数组
const ListView = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
items: arrayOf(object).isRequired,
transformItem: func,
},
getDefaultProps() {
return {
transformItem: item => item,
}
},
render() {
return (
<ul>
{this.props.items.map((item, idx) => {
const transformedItem = this.props.transformItem(item);
return (
<li key={idx}>
<img src={transformedItem.icon} />
<h3>{transformedItem.title}</h3>
<p>{transformedItem.description}</p>
</li>
);
})}
</ul>
);
},
});
这将非常有效。然后,我可以像这样呈现我的用户列表:
<ListView
items={usersList}
transformItem={user => ({
title: user.name,
icon: user.avatar,
description: `Age: ${user.age}`,
})}
/>
ListView只会在usersList
更改时重新呈现。但是我在途中失去了道具类型验证。
解决方案2:创建一个包含ListView
的专用组件:
const UsersList = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
items: arrayOf(shape({
name: string.isRequired,
avatar: string.isRequired,
age: number.isRequired,
})),
},
render() {
return (
<ListView
items={this.props.items.map(user => ({
title: user.name,
icon: user.avatar,
description: user.age,
}))}
/>
);
}
});
但这是更多的代码!如果我可以使用无状态组件会更好,但我还没弄清楚它们是否像PureRenderMixin
的普通组件一样工作。 (无状态组件遗憾地仍然是未记录的反应特征)
const UsersList = ({ users }) => (
<ListView
items={users.map(user => ({
title: user.name,
icon: user.avatar,
description: user.age,
}))}
/>
);
解决方案3:创建通用纯转换组件
const ParameterTransformer = React.createClass({
mixin: [PureRenderMixin],
propTypes: {
component: object.isRequired,
transformers: arrayOf(func).isRequired,
},
render() {
let transformed = {};
this.props.transformers.forEach((transformer, propName) => {
transformed[propName] = transformer(this.props[propName]);
});
return (
<this.props.component
{...this.props}
{...transformed}
>
{this.props.children}
</this.props.component>
);
}
});
并像
一样使用它<ParameterTransformer
component={ListView}
items={usersList}
transformers={{
items: user => ({
title: user.name,
icon: user.avatar,
description: `Age: ${user.age}`,
})
}}
/>
这些是唯一的解决方案,还是我错过了什么?
这些解决方案都不适合我目前正在开发的组件。第一名最合适,但我觉得如果这是我选择继续的轨道,那么我制作的各种通用组件应该具有这些transform
属性。而且,在创建此类组件时,我无法使用React prop类型验证,这是一个主要的缺点。
解决方案2可能是最具语义正确性的,至少在这个ListView
示例中是这样。但我觉得它非常冗长,而且它与我的真实用例并不相符。
解决方案3太神奇且难以理解。
答案 0 :(得分:7)
还有另一种常用于Redux的解决方案:记忆选择器。
像Reselect这样的库提供了一种方法来将函数(如地图调用)包装在另一个函数中,该函数检查调用之间引用相等的参数,并在参数未更改时返回缓存值。
您的方案是备忘选择器的完美示例 - 因为您正在强制执行不变性,您可以假设只要您的usersList
引用相同,就可以调用{{1会是一样的。选择器可以缓存map
函数的返回值,直到map
引用更改为止。
使用Reselect创建一个memoized选择器,首先需要一个(或多个)输入选择器和一个结果函数。输入选择器通常用于从父状态对象中选择子项;在您的情况下,您直接从您拥有的对象中进行选择,因此您可以将身份函数作为第一个参数传递。第二个参数result函数是生成由选择器缓存的值的函数。
usersList
现在,每次React呈现您的var createSelector = require('reselect').createSelector; // ES5
import { createSelector } from 'reselect'; // ES6
var usersListItemSelector = createSelector(
ul => ul, // input selector
ul => ul.map(user => { // result selector
title: user.name,
icon: user.avatar,
description: `Age: ${user.age}`,
})
);
// ...
<ListView items={usersListItemSelector(usersList)} />
组件时,都会调用您的memoized选择器,只有在传入不同的ListView
时才会调用map
结果函数。 / p>