2015-11-17 8 views
7

また、問題をよりよく表現してください。変換が必要な不変のデータを持つ純粋な汎用のReactコンポーネントの作成方法

react/reduxアーキテクチャでは、多くの例が見つからないという問題が発生しました。

私のアプリケーションは、パフォーマンスのために不変性、参照平等およびPureRenderMixinに依存しています。しかし、このようなアーキテクチャでは、より汎用的で再利用可能なコンポーネントを作成することは難しいです。

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

そして私はReduxのストアからユーザーを表示するには、リストビューを使用したい:

は、私はこのような一般的なListViewを考えてみましょう。彼らはこの構造を持っています:

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

これはこれまで私のアプリケーションでは非常に正常なシナリオです。私は通常、それが好きなレンダリング:

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

問題はmapが基準平等を壊すということです。 usersList.map(...)の結果は常に新しいリストオブジェクトになります。したがって、usersListが以前のレンダリングから変更されていない場合でも、ListViewはまだそれがitems -listと等しいことを知ることができません。

私はこれに3つの解決策を見ることができますが、私はそれらがとても好きではありません。

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

とします。

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

そしてusersList変更ListViewコントロールのみ再レンダリングされます:私は、このように私のユーザーのリストをレンダリングすることができます。しかし、私はその途中で小道具タイプの検証を失った。

解決方法2:

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

しかし、それはより多くのコードです:ListViewをラップ専門の部品を作ろう!私はステートレスコンポーネントを使用することができれば良いでしょうが、PureRenderMixinの通常のコンポーネントのように動作するかどうかはわかりません。 (ステートレスコンポーネントはまだ悲しいことに反応するのドキュメント化されていない機能です)

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

解決策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> 
     ); 
    } 
}); 

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

がありますようにそれを使用しますこれらの唯一の解決策、または私は何かを逃したか?

私が現在取り組んでいるコンポーネントには、これらのソリューションはどれも適合しません。ナンバーワンが最適ですが、それが私が続けることを選択したトラックのように感じると、私が作る汎用コンポーネントのすべての種類は、これらのtransformプロパティを持つ必要があります。また、そのようなコンポーネントを作成するときにReact propタイプの検証を使用することができないという大きな欠点もあります。

解決策2は、おそらく最も意味の訂正であり、少なくともこれはListViewの例です。しかし、私はそれが非常に冗長であると思うし、また、私の実際のユースケースにはうまく適合しません。

解決策3はあまりにも魔法であり、判読できません。

答えて

7

Reduxによく使用される別の解決策があります:メモ化セレクタ

Reselectのようなライブラリは、呼び出し間の参照の等価性を調べる引数をチェックする別の関数で(マップ呼び出しのように)関数をラップする方法を提供し、引数が変更されていない場合はキャッシュされた値を返します。

あなたのシナリオはメモ型セレクタの完璧な例です。不変性を強制しているので、usersListの参照が同じである限り、mapへの呼び出しの出力は同じになるという前提を立てることができます。セレクタはusersList参照が変更されるまでmap関数の戻り値をキャッシュできます。

メモ再選択を使用してメモ型セレクタを作成するには、まず1つ(またはそれ以上)の入力セレクタと結果関数が必要です。入力セレクタは、通常、親状態オブジェクトから子項目を選択するために使用されます。あなたの場合、あなたが持っているオブジェクトから直接選択しているので、最初の引数としてID関数を渡すことができます。 2番目の引数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)} /> 

さて、それぞれの時間は、あなたのListViewコンポーネントを描画反応して、あなたのメモ化セレクタが呼び出されます、そして異なるusersListが渡された場合、あなたのmap結果関数のみが呼び出されます。

+0

メモ化、はい、もちろん!どのように私はそれについて考えることができない? アイデアはストローク・ミーですが、それまで考えていなかったので、これまでに選択したすべてのリストの大きなキャッシュになると思いました。しかし、ソースの、最新の結果よりも多くを保持する必要はありませんし、それに対してチェックしてください。 ライブラリを引っ張る代わりに、自分で実装するほうがおそらく簡単です。 –

+0

確かに、自分で実装することができます - 再選択は[コードの82行だけです](https://github.com/rackt/reselect/blob/master/src/index.js#L82)です。 –

関連する問題