コンポーネントは、Stores in Fluxからエンティティをどの入れ子レベルで読み取る必要がありますか?


89

Fluxを使用するようにアプリを書き換えていますが、ストアからのデータの取得に問題があります。私はたくさんのコンポーネントを持っていて、それらはたくさん入れ子になっています。それらのいくつかは大きく(Article)、いくつかは小さくてシンプルです(UserAvatarUserLink)。

私はストアからデータを読み取る必要があるコンポーネント階層のどこに苦労してきました。
私は2つの極端なアプローチを試しましたが、どちらもかなり気に入りました。

すべてのエンティティコンポーネントが独自のデータを読み取る

Storeからのデータを必要とする各コンポーネントは、エンティティIDのみを受け取り、エンティティを独自に取得します。
たとえば、Article渡されarticleIdUserAvatarそしてUserLink渡されますuserId

このアプローチには、いくつかの重大な欠点があります(コードサンプルで説明)。

var Article = React.createClass({
  mixins: [createStoreMixin(ArticleStore)],

  propTypes: {
    articleId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      article: ArticleStore.get(this.props.articleId);
    }
  },

  render() {
    var article = this.state.article,
        userId = article.userId;

    return (
      <div>
        <UserLink userId={userId}>
          <UserAvatar userId={userId} />
        </UserLink>

        <h1>{article.title}</h1>
        <p>{article.text}</p>

        <p>Read more by <UserLink userId={userId} />.</p>
      </div>
    )
  }
});

var UserAvatar = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    userId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      user: UserStore.get(this.props.userId);
    }
  },

  render() {
    var user = this.state.user;

    return (
      <img src={user.thumbnailUrl} />
    )
  }
});

var UserLink = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    userId: PropTypes.number.isRequired
  },

  getStateFromStores() {
    return {
      user: UserStore.get(this.props.userId);
    }
  },

  render() {
    var user = this.state.user;

    return (
      <Link to='user' params={{ userId: this.props.userId }}>
        {this.props.children || user.name}
      </Link>
    )
  }
});

このアプローチの欠点:

  • Storesにサブスクライブする可能性のある100のコンポーネントがあるとイライラします。
  • それはですどのようにデータが更新されており、中にどのような順序のトラックに保つのは難しい各コンポーネントは独立してそのデータを取得するために、
  • エンティティがすでに存在しいる場合でも、そのIDを子に渡さなければなりません。子は再びエンティティを取得します(または整合性を壊します)。

すべてのデータはトップレベルで一度読み込まれ、コンポーネントに渡されます

バグの追跡にうんざりしているとき、私はすべてのデータの取得を最上位に配置しようとしました。ただし、一部のエンティティにはいくつかのレベルのネストがあるため、これは不可能であることが判明しました。

例えば:

  • AにCategoryUserAvatar、そのカテゴリに貢献するsの人々が含まれます。
  • Articleは複数Categoryのがある場合があります。

したがって、のレベルでStoreからすべてのデータを取得したい場合はArticle、次のことを行う必要があります。

  • から記事を取得しArticleStoreます。
  • からすべての記事のカテゴリを取得しCategoryStoreます。
  • から各カテゴリの貢献者を個別に取得しますUserStore
  • なんとかしてすべてのデータをコンポーネントに渡します。

さらにイライラすることに、深くネストされたエンティティが必要な場合は、ネストの各レベルにコードを追加して、さらに渡す必要があります。

まとめ

どちらのアプローチにも欠陥があるようです。この問題を最もエレガントに解決するにはどうすればよいですか?

私の目的:

  • 店舗には、非常に多くの加入者がいるべきではありません。親コンポーネントが既にそれを行っている場合、それぞれUserLinkがリッスンするのは愚かですUserStore

  • 親コンポーネントがストアからオブジェクトを取得した場合(例user:)、ネストされたコンポーネントが再度フェッチする必要はありません。小道具を介して渡すことができるはずです。

  • 関係の追加や削除が複雑になるため、すべてのエンティティ(関係を含む)をトップレベルでフェッチする必要はありません。ネストされたエンティティが新しい関係を取得するたびに(たとえば、カテゴリがを取得するなどcurator)、すべてのネストレベルで新しい小道具を導入したくありません。


1
これを投稿してくれてありがとう。ここに非常によく似た質問を投稿しました:groups.google.com/forum/…私が見たものはすべて、関連するデータではなくフラットなデータ構造を扱ってきました。これに関するベストプラクティスが表示されないことに驚いています。
ウベルラマ2015年

回答:


37

ほとんどの人は、階層の最上部近くにあるコントローラービューコンポーネントで関連するストアを聞くことから始めます。

後で、多くの無関係な小道具が階層を介して深くネストされたコンポーネントに渡されているように見えるとき、一部の人々は、より深いコンポーネントにストア内の変更をリッスンさせることをお勧めします。これにより、コンポーネントツリーのこのより深いブランチについての問題ドメインをより適切にカプセル化できます。これを慎重に行うために行われるべき良い議論があります。

しかし、私は常に一番上で聞いて、単純にすべてのデータを渡すことを好みます。ストアの状態全体を取得して、1つのオブジェクトとして階層全体に渡すこともあり、これを複数のストアに対して行います。したがって、ArticleStoreの状態のプロップとの状態のプロップがありUserStoreます。深くネストされたコントローラービューを回避すると、データの単一のエントリポイントが維持され、データフローが統一されます。それ以外の場合は、複数のデータソースがあり、デバッグが困難になる可能性があります。

この方法ではタイプチェックが難しくなりますが、ReactのPropTypesを使用して、ラージオブジェクトアズプロップに「形状」またはタイプテンプレートを設定できます。参照:https : //github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -検証

ストア間のデータを関連付けるロジックを、ストア自体に配置することもできます。したがって、あなたのArticleStore可能性がwaitFor()ありUserStore、関連するユーザーとArticleそれが提供するすべてのレコードを含めますgetArticles()。ビューでこれを行うと、ロジックをビューレイヤーにプッシュするように聞こえます。これは、可能な限り回避する必要のある方法です。

あなたもを使いたくなるかもしれませんしtransferPropsTo()、多くの人がこれを好むかもしれませんが、私は読みやすさ、したがって保守性のためにすべてを明示的に保つことを好みます。

FWIW、私の理解は、David Nolenが彼のOmフレームワーク(これは多少Flux互換です)で同様のアプローチをとり、ルートノードのデータの単一のエントリポイントを使用することです-Fluxでの同等の操作は、1つのコントローラービューのみです。すべての店を聞いています。これはshouldComponentUpdate()、===を使用して参照で比較できる不変のデータ構造を使用することで効率的になります。不変のデータ構造については、DavidのmoriまたはFacebookのimmutable-jsをチェックアウトしてください。Omに関する私の限られた知識は、主にJavaScript MVCフレームワークの未来から来ています


4
詳しい回答ありがとうございます!店舗の状態の両方のスナップショット全体を渡すことを検討しました。ただし、UserStoreを更新するたびに画面上のすべてのアーティクルが再レンダリングされ、PureRenderMixinはどのアーティクルを更新する必要があるかを通知できないため、これによりパフォーマンスの問題が発生する可能性があります。2番目の提案についても考えましたが、記事のユーザーが各getArticles()呼び出しで「挿入」されている場合、記事の参照の等価性を維持する方法がわかりません。これにより、再びパフォーマンスの問題が発生する可能性があります。
Dan Abramov、2014

1
あなたの要点はわかりますが、解決策はあります。1つは、記事のユーザーIDの配列が変更されたかどうかをshouldComponentUpdate()でチェックできます。これは、基本的に、この特定のケースでPureRenderMixinに本当に必要な機能と動作を手動でローリングするだけです。
fisherwebdev 2014

3
そうです、とにかく、毎分最大数回更新するストアでは当てはまらないパフォーマンスの問題あると確信している場合にのみ、それを行う必要があります。ありがとうございました!
ダンアブラモフ2014

8
作成しているアプリケーションの種類によっては、同じルートに直接属さない「ウィジェット」が画面上に増え始めるとすぐに、1つのエントリポイントを維持するだけで問題が発生します。強制的にそのようにしようとすると、そのルートノードの責任が大きくなりすぎます。クライアント側ですが、KISSとSOLID。
Rygu 2014

37

私が到着したアプローチは、各コンポーネントがそのデータ(IDではなく)を小道具として受け取るようにすることです。ネストされたコンポーネントが関連エンティティを必要とする場合、それを取得するのは親コンポーネント次第です。

この例では、オブジェクトである(おそらくまたはによって取得された)プロップがArticle必要です。articleArticleListArticlePage

ArticleもレンダリングUserLinkしたいのでUserAvatar、記事の作者のUserStoreためauthor: UserStore.get(article.authorId)に、サブスクライブしてその状態を保ちます。その後、レンダリングされますUserLinkと、UserAvatarこれでthis.state.author。彼らがそれをさらに伝えたいなら、彼らはできる。子コンポーネントがこのユーザーを再度取得する必要はありません。

繰り返します:

  • コンポーネントがIDを小道具として受け取ることはありません。すべてのコンポーネントはそれぞれのオブジェクトを受け取ります。
  • 子コンポーネントにエンティティが必要な場合、それを取得して小道具として渡すのは親の責任です。

これは私の問題をかなりうまく解決します。このアプローチを使用するように書き直されたコード例:

var Article = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    article: PropTypes.object.isRequired
  },

  getStateFromStores() {
    return {
      author: UserStore.get(this.props.article.authorId);
    }
  },

  render() {
    var article = this.props.article,
        author = this.state.author;

    return (
      <div>
        <UserLink user={author}>
          <UserAvatar user={author} />
        </UserLink>

        <h1>{article.title}</h1>
        <p>{article.text}</p>

        <p>Read more by <UserLink user={author} />.</p>
      </div>
    )
  }
});

var UserAvatar = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <img src={user.thumbnailUrl} />
    )
  }
});

var UserLink = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <Link to='user' params={{ userId: this.props.user.id }}>
        {this.props.children || user.name}
      </Link>
    )
  }
});

これは、最も内側のコンポーネントを愚かに保ちますが、トップレベルのコンポーネントから地獄を複雑にすることを強制しません。


この回答の上に視点を追加するだけです。概念的には、所有権の概念を開発することができます。データコレクションを考えると、実際にデータを所有する(したがって、そのストアをリッスンする)最上位のビューでファイナライズします。いくつかの例:1. AuthDataはApp Componentで所有される必要があり、Authの変更はすべて、App Componentによってすべての子への小道具として伝達される必要があります。2. ArticleDataは、回答で提案されているようにArticlePageまたはArticle Listが所有する必要があります。これで、ArticleListのすべての子が、実際にデータをフェッチしたコンポーネントに関係なく、ArticleListからAuthDataやArticleDataを受信できます。
Saumitra R. Bhave

2

私の解決策ははるかに簡単です。独自の状態を持つすべてのコンポーネントは、ストアとの会話および聴取が許可されています。これらは非常にコントローラーのようなコンポーネントです。状態を維持せずにレンダリングのみを行う、より深くネストされたコンポーネントは許可されません。彼らは非常にビューのような純粋なレンダリングの小道具のみを受け取ります。

このようにして、すべてがステートフルコンポーネントからステートレスコンポーネントに流れます。ステートフル数を低く保つ。

あなたの場合、Articleはステートフルであり、ストアとの通信やUserLinkなどはレンダリングするだけなので、article.userをpropとして受け取ります。


1
記事にユーザーオブジェクトプロパティがある場合はこれでうまくいくと思いますが、私の場合はuserIdしかありません(実際のユーザーはUserStoreに格納されているため)。または私はあなたのポイントを逃していますか?例を挙げていただけますか?
Dan Abramov、2014

記事でユーザーのフェッチを開始します。または、APIバックエンドを変更してユーザーIDを「拡張可能」にして、ユーザーを一度に取得できるようにします。どちらの方法でも、より深くネストされたコンポーネントをシンプルなビューに保ち、プロップのみに依存します。
Rygu 2014

2
一緒にレンダリングする必要があるため、記事のフェッチを開始する余裕はありません。拡張可能なAPIについては、以前はそれらを使用していましたが、ストアからの解析が非常に困難です。この理由から私は応答を平坦化するためのライブラリーも作成しました。
ダンアブラモフ2014

0

2つの哲学で説明されている問題は、どの単一ページアプリケーションにも共通です。

それらはこのビデオで簡単に説明されています:https : //www.youtube.com/watch?v=IrgHurBjQbg and Relay(https://facebook.github.io/relay)は、あなたが説明するトレードオフを克服するためにFacebookによって開発されました。

リレーのアプローチは非常にデータ中心です。これは、「このビューの各コンポーネントに必要なデータだけをサーバーへの1つのクエリで取得するにはどうすればよいですか」に対する回答です。同時に、Relayは、コンポーネントが複数のビューで使用されている場合、コード間でほとんど結合がないことを確認します。

リレーがオプションはない場合、「すべてのエンティティコンポーネントは独自のデータを読み取る」というのが、あなたが説明する状況にとって私にとってより良いアプローチのようです。Fluxの誤解は、ストアとは何かだと思います。ストアの概念は、モデルまたはオブジェクトのコレクションが保持される場所ではありません。ストアは、ビューがレンダリングされる前にアプリケーションがデータを置く一時的な場所です。それらが存在する本当の理由は、異なるストアに入るデータ全体の依存関係の問題を解決することです。

Fluxが指定していないのは、ストアとモデルの概念およびオブジェクトのコレクションとの関係です(バックボーン)。その意味で、一部の人々は実際にフラックスストアを作成し、ユーザーがブラウザーを開いたままにしている間はフラッシュされない特定のタイプのオブジェクトのコレクションを配置する場所を作っていますが、私がフラックスを理解しているように、それはストアではありませんあるはずです。

解決策は、ビューをレンダリングするために必要なエンティティ(場合によってはさらに多くのエンティティ)が格納され、更新され続ける別のレイヤーを用意することです。このレイヤーがモデルとコレクションを抽象化している場合、サブコンポーネントが独自のデータを取得するために再度クエリを実行する必要があっても問題ありません。


1
Danのアプローチを理解している限り、さまざまなタイプのオブジェクトを保持し、IDを介してそれらを参照することで、これらのオブジェクトのキャッシュを保持し、1か所でのみ更新することができます。たとえば、同じユーザーを参照する記事がいくつかあり、ユーザー名が更新された場合、これをどのように達成できますか?唯一の方法は、すべての記事を新しいユーザーデータで更新することです。これは、バックエンドでドキュメントとリレーショナルデータベースのどちらを選択するかとまったく同じ問題です。それについてどう思いますか?ありがとうございました。
Alex Ponomarev
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.