2015-11-17 14 views
7

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.

Odpowiedz

7

Jest jeszcze inne rozwiązanie, które jest często używane z Redux: przypomniane selektory.

Biblioteki takie jak Reselect umożliwiają objęcie funkcji (np. Wywołanie mapy) w innej funkcji, która sprawdza argumenty pod kątem równości referencyjnej między wywołaniami i zwraca wartość buforowaną, jeśli argumenty są niezmienione.

Twój scenariusz jest doskonałym przykładem dla memoized selektor-skoro jesteś egzekwowania niezmienność można zrobić założenie, że tak długo, jak odniesienie usersList jest taka sama, wyjście z wezwaniem do map będą takie same. Selektor może buforować wartość zwracaną funkcji map, aż zmieni się odniesienie usersList.

Korzystanie z opcji ponownego wyboru w celu utworzenia selektora z pamięcią, wymaga jednego (lub więcej) selektorów wejścia i funkcji wyniku. Selektory wejściowe są zwykle używane do wybierania elementów potomnych z obiektu stanu macierzystego; w twoim przypadku wybierasz bezpośrednio z posiadanego obiektu, więc możesz przekazać funkcję tożsamości jako pierwszy argument. Drugi argument, funkcja wyniku, to funkcja, która generuje wartość, która ma być buforowana przez selektor.

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

Teraz, za każdym razem React renderuje składnik ListView Twój memoized selektor zostanie wywołana, a czynność map wynik zostanie wywołana tylko jeśli inny usersList jest przekazywana.

+0

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. –

+0

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). –

Powiązane problemy