如何使用需要转换的不可变数据制作纯粹的通用React组件

Bro*_*net 7 reactjs redux

还请帮我标题并更好地表达问题.

我在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>
        );
    },
});
Run Code Online (Sandbox Code Playgroud)

然后我想使用列表视图来显示redux商店中的用户.他们有这种结构:

const usersList = [
    { name: 'John Appleseed', age: 18, avatar: 'generic-user.png' },
    { name: 'Kate Example', age: 32, avatar: 'kate.png' },
];
Run Code Online (Sandbox Code Playgroud)

到目前为止,这在我的应用程序中是非常正常的.我通常把它渲染成:

<ListView items={usersList.map(user => ({
    title: user.name,
    icon: user.avatar,
    description: `Age: ${user.age}`,
}))} />
Run Code Online (Sandbox Code Playgroud)

问题是map打破参考平等.结果 usersList.map(...)将永远是新的列表对象.所以即使usersList自上次渲染以来没有改变,ListView也无法知道它仍然具有相同的列表items.

我可以看到三种解决方案,但我不太喜欢它们.

解决方案1:将transformItem-function作为参数传递,并将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>
        );
    },
});
Run Code Online (Sandbox Code Playgroud)

这将非常有效.然后,我可以像这样呈现我的用户列表:

<ListView
    items={usersList}
    transformItem={user => ({
        title: user.name,
        icon: user.avatar,
        description: `Age: ${user.age}`,
    })}
/>
Run Code Online (Sandbox Code Playgroud)

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,
                }))}
            />
        );
    }
});
Run Code Online (Sandbox Code Playgroud)

但那是更多的代码!如果我可以使用无状态组件会更好,但我还没弄清楚它们是否像普通组件一样工作PureRenderMixin.(无状态组件遗憾地仍然是未记录的反应特征)

const UsersList = ({ users }) => (
    <ListView
        items={users.map(user => ({
            title: user.name,
            icon: user.avatar,
            description: user.age,
        }))}
    />
);
Run Code Online (Sandbox Code Playgroud)

解决方案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>
        );
    }
});
Run Code Online (Sandbox Code Playgroud)

并使用它

<ParameterTransformer
    component={ListView}
    items={usersList}
    transformers={{
        items: user => ({
            title: user.name,
            icon: user.avatar,
            description: `Age: ${user.age}`,
        })
    }}
/>
Run Code Online (Sandbox Code Playgroud)

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

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

解决方案2可能是最具语义正确性的,至少在本ListView例中是这样.但我认为它非常冗长,而且它与我的真实用例不太匹配.

解决方案3太神奇,难以理解.

Ada*_*ras 7

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

Reselect这样的库提供了一种方法来将函数(如地图调用)包装在另一个函数中,该函数检查调用之间引用相等的参数,并在参数未更改时返回缓存值.

您的场景是备忘选择器的完美示例 - 因为您强制执行不变性,您可以假设只要您的usersList引用相同,您的调用输出map将是相同的.选择器可以缓存map函数的返回值,直到usersList引用更改为止.

使用Reselect创建一个memoized选择器,首先需要一个(或多个)输入选择器和一个result函数.输入选择器通常用于从父状态对象中选择子项; 在您的情况下,您直接从您拥有的对象中进行选择,因此您可以将身份函数作为第一个参数传递.第二个参数result函数是生成由选择器缓存的值的函数.

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)} />
Run Code Online (Sandbox Code Playgroud)

现在,每次React渲染你的ListView组件时,你的memoized选择器都会被调用,map只有usersList传入不同的结果才会调用你的结果函数.