还请帮我确定标题并更好地表达问题。

我在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> 
        ); 
    }, 
}); 

然后我想使用 ListView 来显示 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 更改时才会重新渲染。但我在途中丢失了 prop 类型验证。

解决方案 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}`, 
        }) 
    }} 
/> 

这些是唯一的解决方案,还是我错过了什么?

这些解决方案都不太适合我当前正在开发的组件。第一个最适合,但我觉得如果这是我选择继续的轨道,那么我制作的各种通用组件都应该具有这些 transform 属性。而且它的一个主要缺点是我在创建此类组件时无法使用 React prop 类型验证。

解决方案 2 可能是语义上最正确的,至少对于这个 ListView 示例来说是这样。但我认为它非常冗长,而且不太适合我的实际用例。

解决方案 3 太神奇且难以理解。

请您参考如下方法:

还有另一种常用于 Redux 的解决方案:内存选择器

类似 Reselect 的库提供一种将函数(例如映射调用)包装在另一个函数中的方法,该函数检查调用之间的参数是否引用相等,如果参数未更改,则返回缓存的值。

您的场景是内存选择器的完美示例 - 因为您正在强制执行不变性,所以您可以假设只要您的 usersList 引用相同,调用的输出map 将是相同的。选择器可以缓存 map 函数的返回值,直到您的 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 组件时,都会调用您的内存选择器,并且只有在不同的 usersList 时才会调用您的 map 结果函数 已传入。


评论关闭
IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!