如何使用需要转换的不可变数据制作纯粹的通用 React 组件
How to make pure, generic React components with immutable data that needs to be transformed
也请大家帮我起个标题,把问题表达的更清楚些。
我在我的 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(...)
将始终是新列表 objects。因此,即使 usersList
自上次渲染以来没有更改,ListView
也不知道它仍然有一个相等的 items
-列表。
我可以看到三个解决方案,但我不太喜欢它们。
解决方案 1:传递一个 transformItem
函数作为参数,让 items
成为原始数组 objects
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
的普通组件一样工作。 (遗憾的是,无状态组件仍然是 React 的一个未记录的特性)
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}`,
})
}}
/>
这些是唯一的解决方案吗,还是我错过了什么?
None 这些解决方案非常适合我目前正在处理的组件。第一个最合适,但我觉得如果这是我选择继续的轨道,那么我制作的各种通用组件都应该具有这些 transform
属性。还有一个主要缺点是我在创建此类组件时无法使用 React prop 类型验证。
解决方案 2 可能是语义最正确的,至少对于这个 ListView
示例而言。但我认为它非常冗长,而且它不太适合我的实际用例。
解决方案 3 太神奇且难以理解。
还有一个 Redux 常用的解决方案:memoized selectors。
像 Reselect 这样的库提供了一种方法来将一个函数(比如你的 map 调用)包装在另一个函数中,该函数检查调用之间的引用相等性的参数,并且 returns 一个缓存值如果参数没有变化。
您的场景是记忆选择器的完美示例 - 因为您强制执行不变性,所以您可以假设只要您的 usersList
引用相同,调用 map
将是相同的。选择器可以缓存 map
函数的 return 值,直到您的 usersList
引用更改。
使用Reselect 创建一个记忆选择器,您首先需要一个(或多个)输入选择器,以及一个结果函数。输入选择器通常用于从父状态对象中选择子项;在您的情况下,您直接从拥有的对象中进行选择,因此您可以将身份函数作为第一个参数传递。第二个参数,结果函数,是生成要被选择器缓存的值的函数。
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)} />
现在,每次 React 渲染你的 ListView
组件时,你的 memoized 选择器都会被调用,而你的 map
结果函数只会在传入不同的 usersList
时被调用.
也请大家帮我起个标题,把问题表达的更清楚些。
我在我的 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(...)
将始终是新列表 objects。因此,即使 usersList
自上次渲染以来没有更改,ListView
也不知道它仍然有一个相等的 items
-列表。
我可以看到三个解决方案,但我不太喜欢它们。
解决方案 1:传递一个 transformItem
函数作为参数,让 items
成为原始数组 objects
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
的普通组件一样工作。 (遗憾的是,无状态组件仍然是 React 的一个未记录的特性)
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}`,
})
}}
/>
这些是唯一的解决方案吗,还是我错过了什么?
None 这些解决方案非常适合我目前正在处理的组件。第一个最合适,但我觉得如果这是我选择继续的轨道,那么我制作的各种通用组件都应该具有这些 transform
属性。还有一个主要缺点是我在创建此类组件时无法使用 React prop 类型验证。
解决方案 2 可能是语义最正确的,至少对于这个 ListView
示例而言。但我认为它非常冗长,而且它不太适合我的实际用例。
解决方案 3 太神奇且难以理解。
还有一个 Redux 常用的解决方案:memoized selectors。
像 Reselect 这样的库提供了一种方法来将一个函数(比如你的 map 调用)包装在另一个函数中,该函数检查调用之间的引用相等性的参数,并且 returns 一个缓存值如果参数没有变化。
您的场景是记忆选择器的完美示例 - 因为您强制执行不变性,所以您可以假设只要您的 usersList
引用相同,调用 map
将是相同的。选择器可以缓存 map
函数的 return 值,直到您的 usersList
引用更改。
使用Reselect 创建一个记忆选择器,您首先需要一个(或多个)输入选择器,以及一个结果函数。输入选择器通常用于从父状态对象中选择子项;在您的情况下,您直接从拥有的对象中进行选择,因此您可以将身份函数作为第一个参数传递。第二个参数,结果函数,是生成要被选择器缓存的值的函数。
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)} />
现在,每次 React 渲染你的 ListView
组件时,你的 memoized 选择器都会被调用,而你的 map
结果函数只会在传入不同的 usersList
时被调用.