セレクターがあります:
const someSelector = createSelector(
getUserIdsSelector,
(ids) => ids.map((id) => yetAnotherSelector(store, id),
); // ^^^^^ (yetAnotherSelector expects 2 args)
yetAnotherSelector
は別のセレクターであり、ユーザーID-id
を取り、データを返します。
ただし、それはcreateSelector
であるため、その中に格納するアクセス権がありません(メモ化が機能しないため、関数としては必要ありません)。
createSelector
内でストアに何らかの方法でアクセスする方法はありますか?または、それに対処する他の方法はありますか?
私には機能があります:
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));
}
そしてそれはトリックですが、それは単なる回避策ではありませんか?
私はあなたと同じケースに直面しましたが、残念ながら別のセレクターのボディからセレクターを呼び出す効率的な方法が見つかりませんでした。
私は効率的な方法と言いました、なぜならあなたは常に状態(ストア)全体を渡す入力セレクターを持つことができますが、これはそれぞれのセレクターを再計算します状態の変更:
const someSelector = createSelector(
getUserIdsSelector,
state => state,
(ids, state) => ids.map((id) => yetAnotherSelector(state, id)
)
しかし、以下で説明するユースケースについて、2つの可能なアプローチを見つけました。あなたのケースは似ていると思うので、いくつかの洞察を得ることができます。
したがって、ケースは次のようになります。IDによってストアから特定のユーザーを取得するセレクターがあり、セレクターは特定の構造でユーザーを返します。 getUserById
セレクターとしましょう。今のところ、すべてが可能な限り素晴らしくシンプルです。ただし、IDで複数のユーザーを取得し、以前のセレクターを再利用する場合に問題が発生します。 getUsersByIds
selectorという名前を付けましょう。
最初の可能な解決策は、常に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] })
ここでの考え方は、セレクター本体の共通で再利用可能な部分をスタンドアロン関数で分離することであるため、この関数は他のすべてのセレクターから呼び出し可能です。
実装は次のとおりです
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
}
}
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
ブランチ全体への依存関係を宣言する必要があります。
これは機能しますが、これは、関係のないユーザー(ボブ、チャーリー、イブ)への変更により、セレクターが再計算されることを意味します。
これは再選択の基本的な設計選択の問題だと思います:セレクター間の依存関係はstaticです。(対照的に、Knockout、VueおよびMobXは動的な依存関係をサポートします。)
同じ問題に直面し、 @taskworld.com/rereselect
を思いつきました。依存関係を事前に静的に宣言する代わりに、依存関係は各計算中にジャストインタイムで動的に収集されます。
これにより、セレクターは、状態のどの部分でセレクターを再計算できるかをよりきめ細かく制御できます。
特定のケースでは、エクステンダーを返すセレクターを作成します。
つまり、このために:
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つのことを行う必要があります。
extendUserDataSelectorSelector
が返す関数を複数メモします。グローバルにメモされた別の関数に抽出することをお勧めします。someFunc
をラップして、配列を返すときに、その配列を要素ごとに前の結果と比較し、同じ要素がある場合は前の結果を返すようにします。上に示したように、グローバルレベルでのキャッシュは確かに実行可能ですが、他のいくつかの戦略を念頭に置いて問題に取り組むと、それを回避できます。
私は自分の主要な仕事のプロジェクトの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
エンティティが多数ある場合、かなり大きくなる可能性があります。
配列はキャッシュの存在の悩みの種です。
arrayOfSomeIds
自体は変更できませんが、ポイント内のIDが持つことができるエンティティは変更できません。arrayOfSomeIds
はメモリ内の新しいオブジェクトかもしれませんが、実際には同じIDを持っています。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に表示されるものを見てみるべきです。特定のケースに役立つ場合と役に立たない場合がある、いくつかの新しいツール。ただし、再選択と再選択だけでなく、ニーズに合わせていくつかの追加機能を使用することで、多くのことができます。