web-dev-qa-db-ja.com

再選択-別のセレクターを呼び出すセレクター?

セレクターがあります:

const someSelector = createSelector(
   getUserIdsSelector,
   (ids) => ids.map((id) => yetAnotherSelector(store, id),
);                                      //     ^^^^^ (yetAnotherSelector expects 2 args)

yetAnotherSelectorは別のセレクターであり、ユーザーID-idを取り、データを返します。

ただし、それはcreateSelectorであるため、その中に格納するアクセス権がありません(メモ化が機能しないため、関数としては必要ありません)。

createSelector内でストアに何らかの方法でアクセスする方法はありますか?または、それに対処する他の方法はありますか?

[〜#〜] edit [〜#〜]

私には機能があります:

const someFunc = (store, id) => {
    const data = userSelector(store, id);
              // ^^^^^^^^^^^^ global selector
    return data.map((user) => extendUserDataSelector(store, user));
                       //     ^^^^^^^^^^^^^^^^^^^^ selector
}

そのような機能は私のアプリを殺し、すべてを再レンダリングさせ、私を夢中にさせます。感謝します。

!!しかしながら:

私はいくつかの基本的な、カスタムのメモ化を行いました。

import { isEqual } from 'lodash';

const memoizer = {};
const someFunc = (store, id) => {
    const data = userSelector(store, id);
    if (id in memoizer && isEqual(data, memoizer(id)) {
       return memoizer[id];
    }

    memoizer[id] = data;
    return memoizer[id].map((user) => extendUserDataSelector(store, user));
}

そしてそれはトリックですが、それは単なる回避策ではありませんか?

9
Patrickkx

序文

私はあなたと同じケースに直面しましたが、残念ながら別のセレクターのボディからセレクターを呼び出す効率的な方法が見つかりませんでした。

私は効率的な方法と言いました、なぜならあなたは常に状態(ストア)全体を渡す入力セレクターを持つことができますが、これはそれぞれのセレクターを再計算します状態の変更:

const someSelector = createSelector(
   getUserIdsSelector,
   state => state,
   (ids, state) => ids.map((id) => yetAnotherSelector(state, id)
)

アプローチ

しかし、以下で説明するユースケースについて、2つの可能なアプローチを見つけました。あなたのケースは似ていると思うので、いくつかの洞察を得ることができます。

したがって、ケースは次のようになります。IDによってストアから特定のユーザーを取得するセレクターがあり、セレクターは特定の構造でユーザーを返します。 getUserByIdセレクターとしましょう。今のところ、すべてが可能な限り素晴らしくシンプルです。ただし、IDで複数のユーザーを取得し、以前のセレクターを再利用する場合に問題が発生します。 getUsersByIds selectorという名前を付けましょう。

1.入力ID値に常に配列を使用する

最初の可能な解決策は、常にidの配列(getUsersByIds)を期待するセレクタと、前のものを再利用する2番目のセレクタを持つことですが、1人のユーザー(getUserById)のみを取得します。そのため、ストアから1人のユーザーのみを取得する場合は、getUserByIdを使用する必要がありますが、1つのユーザーIDのみで配列を渡す必要があります。

実装は次のとおりです

import { createSelectorCreator, defaultMemoize } from 'reselect'
import { isEqual } from 'lodash'

/**
 * Create a "selector creator" that uses `lodash.isEqual` instead of `===`
 *
 * Example use case: when we pass an array to the selectors,
 * they are always recalculated, because the default `reselect` memoize function
 * treats the arrays always as new instances.
 *
 * @credits https://github.com/reactjs/reselect#customize-equalitycheck-for-defaultmemoize
 */
const createDeepEqualSelector = createSelectorCreator(
  defaultMemoize,
  isEqual
)

export const getUsersIds = createDeepEqualSelector(
  (state, { ids }) => ids), ids => ids)

export const getUsersByIds = createSelector(state => state.users, getUsersIds,
  (users, userIds) => {
    return userIds.map(id => ({ ...users[id] })
  }
)

export const getUserById = createSelector(getUsersByIds, users => users[0])

使用法:

// Get 1 User by id
const user = getUserById(state, { ids: [1] })

// Get as many Users as you want by ids
const users = getUsersByIds(state, { ids: [1, 2, 3] }) 

2.セレクターの本体をスタンドアロン機能として再利用します

ここでの考え方は、セレクター本体の共通で再利用可能な部分をスタンドアロン関数で分離することであるため、この関数は他のすべてのセレクターから呼び出し可能です。

実装は次のとおりです

export const getUsersByIds = createSelector(state => state.users, getUsersIds,
  (users, userIds) => {
    return userIds.map(id => _getUserById(users, id))
  }
)

export const getUserById = createSelector(state => state.users, (state, props) => props.id, _getUserById)

const _getUserById = (users, id) => ({ ...users[id]})

使用法:

// Get 1 User by id
const user = getUserById(state, { id: 1 })

// Get as many Users as you want by ids
const users = getUsersByIds(state, { ids: [1, 2, 3] }) 

結論

アプローチ#1。は定型文が少なく(スタンドアロン関数はありません)、クリーンな実装があります。

アプローチ#2。は再利用可能です。セレクターを呼び出すときにユーザーのIDを持たないが、リレーションとしてセレクターの本体から取得する場合を想像してください。その場合、スタンドアロン機能を簡単に再利用できます。疑似例は次のとおりです。

export const getBook = createSelector(state => state.books, state => state.users, (state, props) => props.id,
(books, users, id) => {
  const book = books[id]
  // Here we have the author id (User's id)
  // and out goal is to reuse `getUserById()` selector body,
  // so our solution is to reuse the stand-alone `_getUserById` function.
  const authorId = book.authorId
  const author = _getUserById(users, authorId)

  return {
    ...book,
    author
  }
}
9
Jordan Enev

reselectを使用する際に直面した問題は、動的依存関係追跡のサポートがないことです。セレクターは、状態によって再計算が行われます。

たとえば、オンラインユーザーIDのリストとユーザーのマッピングがあります。

{
  onlineUserIds: [ 'alice', 'dave' ],
  notifications: [ /* unrelated data */ ]
  users: {
    alice: { name: 'Alice' },
    bob: { name: 'Bob' },
    charlie: { name: 'Charlie' },
    dave: { name: 'Dave' },
    eve: { name: 'Eve' }
  }
}

オンラインユーザーのリストを選択したい[ { name: 'Alice' }, { name: 'Dave' } ]

どのユーザーがオンラインになるかを事前に知ることができないため、ストアのstate.usersブランチ全体への依存関係を宣言する必要があります。

Example 1

これは機能しますが、これは、関係のないユーザー(ボブ、チャーリー、イブ)への変更により、セレクターが再計算されることを意味します。

これは再選択の基本的な設計選択の問題だと思います:セレクター間の依存関係はstaticです。(対照的に、Knockout、VueおよびMobXは動的な依存関係をサポートします。)

同じ問題に直面し、 @taskworld.com/rereselect を思いつきました。依存関係を事前に静的に宣言する代わりに、依存関係は各計算中にジャストインタイムで動的に収集されます。

Example 2

これにより、セレクターは、状態のどの部分でセレクターを再計算できるかをよりきめ細かく制御できます。

8
Thai

SomeFuncケース用

特定のケースでは、エクステンダーを返すセレクターを作成します。

つまり、このために:

const someFunc = (store, id) => {
    const data = userSelector(store, id);
              // ^^^^^^^^^^^^ global selector
    return data.map((user) => extendUserDataSelector(store, user));
                       //     ^^^^^^^^^^^^^^^^^^^^ selector
}

私は書くだろう:

const extendUserDataSelectorSelector = createSelector(
  selectStuffThatExtendUserDataSelectorNeeds,
  (state) => state.something.else.it.needs,
  (stuff, somethingElse) =>
    // This function will be cached as long as
    // the results of the above two selectors
    // does not change, same as with any other cached value.
    (user) => {
      // your magic goes here.
      return {
        // ... user with stuff and somethingElse
      };
    }
);

someFuncは次のようになります。

const someFunc = createSelector(
  userSelector,
  extendUserDataSelectorSelector,
  // I prefix injected functions with a $.
  // It's not really necessary.
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
);

現在の状態に事前にバインドされた関数を作成し、単一の入力を受け入れてそれを修飾するため、これをreifierパターンと呼びます。私は通常、IDで物事を取得するためにそれを使用しました。そのため、「reify」を使用します。私はまた、「具体化」と言うのが好きです。それが正直に言って、私がそれを呼ぶ主な理由です。

あなたのただしケース

この場合:

import { isEqual } from 'lodash';

const memoizer = {};
const someFunc = (store, id) => {
    const data = userSelector(store, id);
    if (id in memoizer && isEqual(data, memoizer(id)) {
       return memoizer[id];
    }

    memoizer[id] = data;
    return memoizer[id].map((user) => extendUserDataSelector(store, user));
}

それは基本的に re-reselect が行うことです。グローバルレベルでIDごとのメモ化の実装を計画している場合は、そのことを検討してください。

import createCachedSelector from 're-reselect';

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
// NOTE THIS PART DOWN HERE!
// This is how re-reselect gets the cache key.
)((state, id) => id);

または、基本的に同じものなので、メモ化されたマルチセレクター作成者を弓で包んでcreateCachedSelectorと呼ぶこともできます。

編集:関数を返す理由

これを行う別の方法は、extendUserDataSelector計算の実行に必要なすべての適切なデータを選択することですが、これはその計算を使用する他のすべての関数をインターフェイスに公開することを意味します。単一のuser基本データのみを受け入れる関数を返すことにより、他のセレクターのインターフェイスをクリーンに保つことができます。

編集:コレクションについて

上記の実装が現在脆弱なのは、依存関係セレクターが変更されたためにextendUserDataSelectorSelectorの出力が変更されたが、userSelectorによって取得されたユーザーデータが変更されず、実際の計算されたエンティティも作成されなかった場合ですextendUserDataSelectorSelectorによって。そのような場合、次の2つのことを行う必要があります。

  1. extendUserDataSelectorSelectorが返す関数を複数メモします。グローバルにメモされた別の関数に抽出することをお勧めします。
  2. someFuncをラップして、配列を返すときに、その配列を要素ごとに前の結果と比較し、同じ要素がある場合は前の結果を返すようにします。

編集:キャッシュをあまり避ける

上に示したように、グローバルレベルでのキャッシュは確かに実行可能ですが、他のいくつかの戦略を念頭に置いて問題に取り組むと、それを回避できます。

  1. データを熱心に拡張せず、実際にデータ自体をレンダリングしている各React(または他のビュー)コンポーネントに延期します。
  2. Id/base-objectsのリストを拡張バージョンに熱心に変換せずに、親がそれらのid/base-objectsを子に渡すようにします。

私は自分の主要な仕事のプロジェクトの1つで最初はそれらをフォローしていませんでした。現状では、すべてのビューをリファクタリングするよりも簡単に修正できるため、後で行う必要がありますが、現在は時間/予算が不足しているため、グローバルメモ化のルートに後で行く必要がありました。

編集2(または4と思いますか):コレクションの再検討pt。 1:エクステンダーのマルチメモ化

注:この部分を説明する前に、Extenderに渡されるBase Entityには、一意に識別するために使用できる何らかの種類のidプロパティがあるか、または何らかの類似のプロパティがそれに由来して安く。

このために、他のセレクターと同様の方法で、エクステンダー自体をメモします。ただし、Extenderにその引数についてメモしてもらいたいので、Stateを直接渡す必要はありません。

基本的に、セレクタで re-reselect と同じように機能するMulti-Memoizerが必要です。実際、createCachedSelectorを使用してそれを実行するのは簡単です。

function cachedMultiMemoizeN(n, cacheKeyFn, fn) {
  return createCachedSelector(
    // NOTE: same as [...new Array(n)].map((e, i) => Lodash.nthArg(i))
    [...new Array(n)].map((e, i) => (...args) => args[i]),
    fn
  )(cacheKeyFn);
}

function cachedMultiMemoize(cacheKeyFn, fn) {
  return cachedMultiMemoizeN(fn.length, cacheKeyFn, fn);
}

次に、古いextendUserDataSelectorSelectorの代わりに:

const extendUserDataSelectorSelector = createSelector(
  selectStuffThatExtendUserDataSelectorNeeds,
  (state) => state.something.else.it.needs,
  (stuff, somethingElse) =>
    // This function will be cached as long as
    // the results of the above two selectors
    // does not change, same as with any other cached value.
    (user) => {
      // your magic goes here.
      return {
        // ... user with stuff and somethingElse
      };
    }
);

次の2つの機能があります。

// This is the main caching workhorse,
// creating a memoizer per `user.id`
const extendUserData = cachedMultiMemoize(
  // Or however else you get globally unique user id.
  (user) => user.id,
  function $extendUserData(user, stuff, somethingElse) {
    // your magic goes here.
    return {
      // ...user with stuff and somethingElse
    };
  }
);

// This is still wrapped in createSelector mostly as a convenience.
// It doesn't actually help much with caching.
const extendUserDataSelectorSelector = createSelector(
  selectStuffThatExtendUserDataSelectorNeeds,
  (state) => state.something.else.it.needs,
  (stuff, somethingElse) =>
    // This function will be cached as long as
    // the results of the above two selectors
    // does not change, same as with any other cached value.
    (user) => extendUserData(
      user,
      stuff,
      somethingElse
    )
);

extendUserDataは実際のキャッシングが発生する場所ですが、公正な警告です。baseUserエンティティが多数ある場合、かなり大きくなる可能性があります。

編集2(または4と思いますか):コレクションの再検討pt。 2:配列

配列はキャッシュの存在の悩みの種です。

  1. arrayOfSomeIds自体は変更できませんが、ポイント内のIDが持つことができるエンティティは変更できません。
  2. arrayOfSomeIdsはメモリ内の新しいオブジェクトかもしれませんが、実際には同じIDを持っています。
  3. arrayOfSomeIdsは変更されませんでしたが、参照先エンティティを保持するコレクションは変更されましたが、これらの特定のIDによって参照される特定のエンティティは変更されませんでした。

それが、配列(およびその他のコレクション)の拡張/拡張/具体化/ whatelelseificationをデータ取得、派生、ビューのレンダリングプロセスのできるだけ遅い段階に委任することを提唱する理由です:扁桃体の痛みこのすべてを検討します。

とはいえ、それは不可能ではなく、単に追加のチェックが必要です。

上記のキャッシュバージョンのsomeFuncから開始します。

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
// NOTE THIS PART DOWN HERE!
// This is how re-reselect gets the cache key.
)((state, id) => id);

次に、出力をキャッシュするだけの別の関数でそれをラップできます。

function keepLastIfEqualBy(isEqual) {
  return function $keepLastIfEqualBy(fn) {
    let lastValue;

    return function $$keepLastIfEqualBy(...args) {
      const nextValue = fn(...args);
      if (! isEqual(lastValue, nextValue)) {
        lastValue = nextValue;
      }
      return lastValue;
    };
  };
}

function isShallowArrayEqual(a, b) {
  if (a === b) return true;
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    // NOTE: calling .every on an empty array always returns true.
    return a.every((e, i) => e === b[i]);
  }
  return false;
}

さて、これをcreateCachedSelectorの結果に適用することはできません。これは1セットの出力にのみ適用されます。むしろ、createCachedSelectorが作成する基礎となるセレクタごとに使用する必要があります。幸いなことに、再選択すると、使用するセレクタ作成者を構成できます。

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
)((state, id) => id,
  // NOTE: Second arg to re-reselect: options object.
  {
    // Wrap each selector that createCachedSelector itself creates.
    selectorCreator: (...args) =>
      keepLastIfEqualBy(isShallowArrayEqual)(createSelector(...args)),
  }
)

ボーナスパート:配列入力

ケース1と3をカバーする配列出力のみをチェックしていることに気づいたかもしれませんが、これで十分かもしれません。ただし、場合によっては、キャッチケース2も必要になり、入力配列を確認する必要があります。これは、reselectのcreateSelectorCreatorを使用して カスタムの等式関数を使用して独自のcreateSelectorを作成

import { createSelectorCreator, defaultMemoize } from 'reselect';

const createShallowArrayKeepingSelector = createSelectorCreator(
  defaultMemoize,
  isShallowArrayEqual
);

// Also wrapping with keepLastIfEqualBy() for good measure.
const createShallowArrayAwareSelector = (...args) =>
  keepLastIfEqualBy(
    isShallowArrayEqual
  )(
    createShallowArrayKeepingSelector(...args)
  );

// Or, if you have lodash available,
import compose from 'lodash/fp/compose';
const createShallowArrayAwareSelector = compose(
  keepLastIfEqualBy(isShallowArrayEqual),
  createSelectorCreator(defaultMemoize, isShallowArrayEqual)
);

someFuncを変更するだけで、selectorCreator定義がさらに変更されます。

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
)((state, id) => id, {
  selectorCreator: createShallowArrayAwareSelector,
});

他の考え

それはすべて、reselectおよびre-reselectを検索するときにnpmに表示されるものを見てみるべきです。特定のケースに役立つ場合と役に立たない場合がある、いくつかの新しいツール。ただし、再選択と再選択だけでなく、ニーズに合わせていくつかの追加機能を使用することで、多くのことができます。

7
Joseph Sikorski