Fluxアーキテクチャでは、Storeのライフサイクルをどのように管理するのですか?

Flux]1を読んでいますが、Todoアプリの例があまりにも単純すぎて、いくつかの重要なポイントを理解することができません。

Facebookのような、ユーザープロファイルページを持つシングルページアプリを想像してください。各ユーザープロファイルページでは、ユーザ情報と直近の投稿を、無限スクロールで表示したいと思います。あるユーザープロファイルから別のユーザープロファイルに移動することができます。

Fluxのアーキテクチャでは、これはStoreとDispatcherにどう対応するのでしょうか?

ユーザーごとに1つの PostStore を使うのか、それともある種のグローバルなストアを持つのか? ディスパッチャについては、「ユーザーページ」ごとに新しいディスパッチャを作成するのか、それともシングルトンを使用するのか?最後に、ルート変更に対応する「ページ固有の」ストアのライフサイクルを管理するのは、アーキテクチャのどの部分でしょうか?

さらに、1つの擬似ページが、同じ型のデータのリストを複数持つことがあります。例えば、プロフィールページで、FollowersFollowsの両方を表示したいです。この場合、シングルトンの UserStore はどのように機能するのでしょうか?UserPageStorefollowedBy.UserStoreを管理するのでしょうか?UserStorefollows:UserStore は管理できますか?

ソリューション

Fluxアプリでは、Dispatcherは1つだけであるべきです。 すべてのデータはこの中心的なハブを経由して流れます。 シングルトンのDispatcherを持つことで、すべてのStoreを管理することができます。 これは、Store #1が自身を更新し、Store #2がActionとStore #1の状態の両方を基に自身を更新する必要がある場合に重要になります。 Fluxは、このような状況が大規模なアプリケーションで起こりうることを想定しています。 理想を言えば、このような状況は発生する必要がなく、開発者は可能であればこのような複雑な状況を避けるように努力すべきです。 しかし、シングルトンDispatcherは、いざというときに対処できるようになっています。

ストアも同様にシングルトンです。 ストアは可能な限り独立かつ非結合のままであるべきで、Controller-Viewから問い合わせることができる自己完結した宇宙です。 ストアに入るには、ディスパッチャに登録されたコールバックを経由する必要があります。 Storeに入るには、Dispatcherに登録したコールバックを経由し、出るにはゲッター関数を経由します。 Storeは状態が変化したときにイベントも発行するので、Controller-Viewはゲッターを使って新しい状態を問い合わせるタイミングを知ることができます。

この例では、1つの PostStore が存在することになります。 この同じストアで、FB のニュースフィードのようなページ(疑似ページ)の投稿を管理できます。 論理的なドメインは投稿のリストであり、どんな投稿のリストも扱うことができます。 擬似ページから擬似ページに移行する際には、新しい状態を反映させるために、ストアの状態を再初期化したいと思います。 擬似ページ間を行き来するための最適化として、以前の状態をlocalStorageにキャッシュすることもできますが、私の考えでは、他のすべてのストアを待ち、擬似ページ上のすべてのストアについてlocalStorageとの関係を管理し、それから自身の状態を更新する PageStore をセットアップすることをお勧めします。 この PageStore は投稿については何も保存しないことに注意してください -- それは PostStore の領域です。 これは単に、特定の擬似ページがキャッシュされているかどうかを知っているだけです。なぜなら、擬似ページはそのドメインだからです。

PostStoreinitialize()メソッドを持っています。 このメソッドは、たとえ最初の初期化であっても、常に古い状態をクリアし、Actionを通して受け取ったデータに基づいて、Dispatcherを介して状態を作成することになる。 ある疑似ページから別の疑似ページへの移動には、おそらくPAGE_UPDATEアクションが必要で、これがinitialize()` の起動のトリガーとなるでしょう。 ローカルキャッシュからのデータ取得、サーバーからのデータ取得、楽観的なレンダリング、XHRエラーの状態などの詳細な作業が必要ですが、これが一般的なアイデアです。

もし、特定の擬似ページがアプリケーション内のすべてのストアを必要としないのであれば、メモリの制約以外に、未使用のストアを破棄する理由があるとは全く思えません。 しかし、ストアは通常、大量のメモリを消費するわけではありません。 ただ、破棄するController-Viewsのイベントリスナーを確実に削除する必要があります。 これはReactの componentWillUnmount() メソッドで行われます。

解説 (9)

(注:JSX Harmonyオプションを使用して、ES6構文を使用しています)。

練習として、Github usersとreposをブラウズできる sample Flux app を書きました。
これはfisherwebdev's answerに基づいていますが、私がAPIのレスポンスを正規化するために使っているアプローチも反映しています。

Fluxの学習中に私が試したいくつかのアプローチを記録するために作りました。
現実の世界に近づけようとしました(ページネーション、偽のlocalStorage APIを使わない)。

この中で、特に興味を持った部分がいくつかあります。

  • Fluxアーキテクチャ](https://github.com/facebook/flux)と[react-router](https://github.com/rackt/react-router)を使用しています
  • ユーザページに部分的な既知の情報を表示し、詳細を読み込むことができます。
  • ユーザーとレポのページネーションに対応しています。
  • Github'のネストされたJSONレスポンスをnormalizrでパースします。
  • コンテンツストア don't 必要なのはアクションを含む巨大な switch です;
  • 全てのデータはStoresにあるので、"戻る "は即座にできます。

ストアの分類方法

他のFluxの例で見た、特にStoresの重複を避けようとしました。 特にStoresについては、他のFluxの例で見られたような重複を避けるようにしました。Storesを論理的に3つのカテゴリーに分けることが有効であると考えました。

コンテンツストアは、すべてのアプリのエンティティを格納します。コンテンツストア**は、すべてのアプリのエンティティを保持します。IDを持つすべてのものは、それ自身のコンテンツストアが必要です。個々のアイテムをレンダリングするコンポーネントは、新鮮なデータをコンテンツ ストアに要求します。

コンテンツ ストアは すべての サーバー アクションからオブジェクトを取得します。例えば、UserStoreaction.response.entities.users が存在すれば、どのアクションが実行されたかに関係なく、そのオブジェクトを取得することができます。スイッチ`は必要ありません。Normalizr を使えば、どんなAPIレスポンスも簡単にこの形式にすることができます。

// Content Stores keep their data like this
{
  7: {
    id: 7,
    name: 'Dan'
  },
  ...
}

リストストア は、グローバルなリスト (例: "feed", "your notifications") に出現するエンティティの ID を記録します。このプロジェクトでは、そのようなストアはありませんが、とりあえず触れておこうと思います。これらはページネーションを扱います。

通常、いくつかのアクション(例: REQUEST_FEED, REQUEST_FEED_SUCCESS, REQUEST_FEED_ERROR )に反応するだけです。

// Paginated Stores keep their data like this
[7, 10, 5, ...]

インデックス付きリストストア はリストストアに似ていますが、1対多のリレーションシップを定義します。例えば、"user's subscribers", "repository's stargazers", "user's repositories "のような関係です。また、ページネーションも扱えます。

また、通常はいくつかのアクションに反応します (例: REQUEST_USER_REPOSREQUEST_USER_REPOS_SUCCESSREQUEST_USER_REPOS_ERROR など)。

ほとんどのソーシャルアプリでは、このようなものがたくさんあり、それらのうちの1つを素早く作成できるようにしたいと思うでしょう。

// Indexed Paginated Stores keep their data like this
{
  2: [7, 10, 5, ...],
  6: [7, 1, 2, ...],
  ...
}

注:これらは実際のクラスなどではなく、私がStoreについて考えたい方法です。 でも、いくつかヘルパーを作りました。

StoreUtils

createStore (ストア作成)

このメソッドは、最も基本的なStoreを提供します。

createStore(spec) {
  var store = merge(EventEmitter.prototype, merge(spec, {
    emitChange() {
      this.emit(CHANGE_EVENT);
    },

    addChangeListener(callback) {
      this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener(callback) {
      this.removeListener(CHANGE_EVENT, callback);
    }
  }));

  _.each(store, function (val, key) {
    if (_.isFunction(val)) {
      store[key] = store[key].bind(store);
    }
  });

  store.setMaxListeners(0);
  return store;
}

私はすべてのStoreを作成するためにこの方法を使用しています。

isInBag, mergeIntoBag.

コンテンツストアに便利な小さなヘルパーです。

isInBag(bag, id, fields) {
  var item = bag[id];
  if (!bag[id]) {
    return false;
  }

  if (fields) {
    return fields.every(field => item.hasOwnProperty(field));
  } else {
    return true;
  }
},

mergeIntoBag(bag, entities, transform) {
  if (!transform) {
    transform = (x) => x;
  }

  for (var key in entities) {
    if (!entities.hasOwnProperty(key)) {
      continue;
    }

    if (!bag.hasOwnProperty(key)) {
      bag[key] = transform(entities[key]);
    } else if (!shallowEqual(bag[key], entities[key])) {
      bag[key] = transform(merge(bag[key], entities[key]));
    }
  }
}

PaginatedList

ページ送りの状態を保存し、特定のアサーション(can't fetch during fetch page, etc)を強制する。

class PaginatedList {
  constructor(ids) {
    this._ids = ids || [];
    this._pageCount = 0;
    this._nextPageUrl = null;
    this._isExpectingPage = false;
  }

  getIds() {
    return this._ids;
  }

  getPageCount() {
    return this._pageCount;
  }

  isExpectingPage() {
    return this._isExpectingPage;
  }

  getNextPageUrl() {
    return this._nextPageUrl;
  }

  isLastPage() {
    return this.getNextPageUrl() === null && this.getPageCount() > 0;
  }

  prepend(id) {
    this._ids = _.union([id], this._ids);
  }

  remove(id) {
    this._ids = _.without(this._ids, id);
  }

  expectPage() {
    invariant(!this._isExpectingPage, 'Cannot call expectPage twice without prior cancelPage or receivePage call.');
    this._isExpectingPage = true;
  }

  cancelPage() {
    invariant(this._isExpectingPage, 'Cannot call cancelPage without prior expectPage call.');
    this._isExpectingPage = false;
  }

  receivePage(newIds, nextPageUrl) {
    invariant(this._isExpectingPage, 'Cannot call receivePage without prior expectPage call.');

    if (newIds.length) {
      this._ids = _.union(this._ids, newIds);
    }

    this._isExpectingPage = false;
    this._nextPageUrl = nextPageUrl || null;
    this._pageCount++;
  }
}

PaginatedStoreUtils

createListStore, createIndexedListStore, createListActionHandler を作成する。

定型的なメソッドとアクション処理を提供することで、 Indexed List Store の作成を可能な限りシンプルにします。

var PROXIED_PAGINATED_LIST_METHODS = [
  'getIds', 'getPageCount', 'getNextPageUrl',
  'isExpectingPage', 'isLastPage'
];

function createListStoreSpec({ getList, callListMethod }) {
  var spec = {
    getList: getList
  };

  PROXIED_PAGINATED_LIST_METHODS.forEach(method => {
    spec[method] = function (...args) {
      return callListMethod(method, args);
    };
  });

  return spec;
}

/**
 * Creates a simple paginated store that represents a global list (e.g. feed).
 */
function createListStore(spec) {
  var list = new PaginatedList();

  function getList() {
    return list;
  }

  function callListMethod(method, args) {
    return list[method].call(list, args);
  }

  return createStore(
    merge(spec, createListStoreSpec({
      getList: getList,
      callListMethod: callListMethod
    }))
  );
}

/**
 * Creates an indexed paginated store that represents a one-many relationship
 * (e.g. user's posts). Expects foreign key ID to be passed as first parameter
 * to store methods.
 */
function createIndexedListStore(spec) {
  var lists = {};

  function getList(id) {
    if (!lists[id]) {
      lists[id] = new PaginatedList();
    }

    return lists[id];
  }

  function callListMethod(method, args) {
    var id = args.shift();
    if (typeof id ===  'undefined') {
      throw new Error('Indexed pagination store methods expect ID as first parameter.');
    }

    var list = getList(id);
    return list[method].call(list, args);
  }

  return createStore(
    merge(spec, createListStoreSpec({
      getList: getList,
      callListMethod: callListMethod
    }))
  );
}

/**
 * Creates a handler that responds to list store pagination actions.
 */
function createListActionHandler(actions) {
  var {
    request: requestAction,
    error: errorAction,
    success: successAction,
    preload: preloadAction
  } = actions;

  invariant(requestAction, 'Pass a valid request action.');
  invariant(errorAction, 'Pass a valid error action.');
  invariant(successAction, 'Pass a valid success action.');

  return function (action, list, emitChange) {
    switch (action.type) {
    case requestAction:
      list.expectPage();
      emitChange();
      break;

    case errorAction:
      list.cancelPage();
      emitChange();
      break;

    case successAction:
      list.receivePage(
        action.response.result,
        action.response.nextPageUrl
      );
      emitChange();
      break;
    }
  };
}

var PaginatedStoreUtils = {
  createListStore: createListStore,
  createIndexedListStore: createIndexedListStore,
  createListActionHandler: createListActionHandler
};

createStoreMixin

コンポーネントが興味のあるStoreにチューニングするためのMixinです。例: mixins: [createStoreMixin(UserStore)].

function createStoreMixin(...stores) {
  var StoreMixin = {
    getInitialState() {
      return this.getStateFromStores(this.props);
    },

    componentDidMount() {
      stores.forEach(store =>
        store.addChangeListener(this.handleStoresChanged)
      );

      this.setState(this.getStateFromStores(this.props));
    },

    componentWillUnmount() {
      stores.forEach(store =>
        store.removeChangeListener(this.handleStoresChanged)
      );
    },

    handleStoresChanged() {
      if (this.isMounted()) {
        this.setState(this.getStateFromStores(this.props));
      }
    }
  };

  return StoreMixin;
}
解説 (10)

そこでRefluxでは、Dispatcherの概念を取り除き、アクションとストアによるデータフローだけで考えるようにしました。すなわち


Actions 
解説 (2)