Proszę również pomóc mi w tytule i lepiej wyrazić problem.Jak wykonać czysty, ogólny komponent React z niezmiennymi danymi, które należy przekształcić
Znalazłem problem w mojej architekturze react/redux, której nie mogę znaleźć wiele przykładów.
Moja aplikacja polega na niezmienności, równości referencyjnej i PureRenderMixin
dla wydajności. Ale trudno jest tworzyć bardziej ogólne i nadające się do ponownego wykorzystania komponenty o takiej architekturze.
Powiedzmy mam rodzajowe ListView
takiego:
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>
);
},
});
A potem chcę użyć widoku listy do wyświetlania użytkownikom sklepu Redux. Mają tę strukturę:
const usersList = [
{ name: 'John Appleseed', age: 18, avatar: 'generic-user.png' },
{ name: 'Kate Example', age: 32, avatar: 'kate.png' },
];
To jest normalny senario w moich dotychczasowych aplikacjach. I zazwyczaj czynią to lubią:
<ListView items={usersList.map(user => ({
title: user.name,
icon: user.avatar,
description: `Age: ${user.age}`,
}))} />
Problem jest to, że map
łamie równości odniesienia. Wynik usersList.map(...)
będzie zawsze nowymi obiektami listy. Tak więc nawet jeśli usersList
nie zmieniło się od czasu poprzedniego renderowania, ListView
nie może wiedzieć, że wciąż ma taką samą listę: .
Widzę trzy rozwiązania tego problemu, ale nie lubię ich bardzo dobrze.
Roztwór 1: Przepuścić transformItem
-function jako parametr i pozwolić items
się tablicę surowego obiektów
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>
);
},
});
To bardzo dobrze. Mogę wtedy czynią moje listy użytkowników tak:
<ListView
items={usersList}
transformItem={user => ({
title: user.name,
icon: user.avatar,
description: `Age: ${user.age}`,
})}
/>
A ListView będzie tylko rerender kiedy usersList
zmiany. Ale po drodze zgubiłem walidację typu podpory.
Rozwiązanie 2: Sprawdź, wyspecjalizowany komponent, która otacza 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,
}))}
/>
);
}
});
ale to dużo więcej kodu! Byłoby lepiej, gdybym mógł użyć elementu bezpaństwowego, ale nie zorientowałem się, czy działają jak normalne komponenty z PureRenderMixin
. (Bezpaństwowe składników jest niestety nadal Nieudokumentowana cechą reakcji)
const UsersList = ({ users }) => (
<ListView
items={users.map(user => ({
title: user.name,
icon: user.avatar,
description: user.age,
}))}
/>
);
Roztwór 3: Tworzenie ogólny czystego składnika transformującej
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>
);
}
});
i stosować jak
<ParameterTransformer
component={ListView}
items={usersList}
transformers={{
items: user => ({
title: user.name,
icon: user.avatar,
description: `Age: ${user.age}`,
})
}}
/>
Są te jedyne rozwiązania, czy coś przeoczyłem?
Żadne z tych rozwiązań nie pasuje do komponentu, nad którym obecnie pracuję. Numer jeden pasuje najlepiej, ale mam wrażenie, że jeśli jest to ścieżka, którą wybieram, to wszystkie rodzaje generycznych składników, które tworzę, powinny mieć te właściwości transform
. A także jest poważnym minusem, że nie mogę użyć sprawdzania poprawności typu prop React podczas tworzenia takich komponentów.
Rozwiązanie 2 jest prawdopodobnie najbardziej semantycznym poprawnym, przynajmniej dla tego przykładu ListView
. Ale myślę, że jest bardzo gadatliwy, a także nie pasuje do mojej prawdziwej przydatności.
Rozwiązanie 3 jest zbyt magiczne i nieczytelne.
memoization, tak, oczywiście ! Jak mogłem o tym nie myśleć? Albo pomysł mnie pogłaskał, ale nie przemyślałem tego (w zasadzie nigdy nie potrzebowałem wcześniejszej memoizacji), więc pomyślałem, że spowoduje to duży bufor wszystkich wcześniej wybranych list. Ale z odwagi, nie trzeba trzymać więcej niż najnowszy wynik i sprawdzić przeciwko temu. Memoization jest prawdopodobnie łatwiejsze do zrealizowania niż do biblioteki. –
Oczywiście, możesz to zaimplementować samodzielnie-Ponowne wybieranie to [tylko 82 linii kodu] (https://github.com/rackt/reselect/blob/master/src/index.js#L82). –