Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make pure, generic React components with immutable data that needs to be transformed

Tags:

reactjs

redux

Please also help me with the title and to express the problem better.

I've met a problem in my react/redux architecture that I can't find many examples on.

My application relies on immutability, reference equality and PureRenderMixin for performance. But I find it hard to create more generic and reusable components with such architecture.

Lets say I have a generic ListView like this:

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

And then I want to use the list view to display users from a redux store. They have this structure:

const usersList = [
    { name: 'John Appleseed', age: 18, avatar: 'generic-user.png' },
    { name: 'Kate Example', age: 32, avatar: 'kate.png' },
];

This is a very normal senario in my applications so far. I usually render it like:

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

Problem is that map breaks the reference equality. The result of usersList.map(...) will always be new list objects. So even when usersList hasn't changed since previous render, the ListView can't know that it still has a equal items-list.

I can see three solutions to this, but I don't like them very good.

Solution 1: Pass a transformItem-function as a parameter and let items be an array of raw 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>
        );
    },
});

This will work very well. I can then render my user list like this:

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

And the ListView will only rerender when usersList changes. But I lost the prop type validation on the way.

Solution 2: Make a specialized component that wraps the 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,
                }))}
            />
        );
    }
});

But that is a lot more code! Would be better if I could use a stateless component, but I haven't figured out if they works like normal components with PureRenderMixin. (stateless components is sadly still an undocumented feature of react)

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

Solution 3: Create a generic pure transforming component

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>
        );
    }
});

and use it like

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

Are these the only solutions, or have I missed something?

None of these solutions fits very well with the component I'm currently working on. Number one fits best, but I feel like if that is the track I choose to go on, then all kinds of generic components I make should have these transform properties. And also it is a major downside that I can't use React prop type validation when creating such components.

Solution 2 is probably the most semantic correct, at least for this ListView example. But I think it is very verbose, and also it doesn't fit very well with my real usecase.

Solution 3 is just too magic and unreadable.

like image 219
Brodie Garnet Avatar asked Nov 17 '15 14:11

Brodie Garnet


1 Answers

There's another solution that's commonly used with Redux: memoized selectors.

Libraries like Reselect provide a way to wrap a function (like your map call) in another function that checks the arguments for reference equality between calls, and returns a cached value if the arguments are unchanged.

Your scenario is a perfect example for a memoized selector—since you're enforcing immutability, you can make the assumption that as long as your usersList reference is the same, the output from your call to map will be the same. The selector can cache the return value of the map function until your usersList reference changes.

Using Reselect to create a memoized selector, you first need one (or more) input selectors, and a result function. The input selectors are generally used for selecting child items from a parent state object; in your case, you're selecting directly from the object you have, so you can pass the identity function as the first argument. The second argument, the result function, is the function that generates the value to be cached by the selector.

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)} />

Now, each time React renders your ListView component, your memoized selector will be called, and your map result function will only be called if a different usersList is passed in.

like image 139
Adam Maras Avatar answered Oct 19 '22 19:10

Adam Maras