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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With