Повторный выбор - селектор, который вызывает другой селектор?

У меня есть селектор:

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

Тот yetAnotherSelector это другой селектор, который принимает идентификатор пользователя - 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));
}

И это помогает, но разве это не обходной путь?

6 ответов

Для вашего случая

Для вашего конкретного случая я бы создал селектор, который сам возвращает экстендер.

То есть для этого:

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, потому что он создает функцию, которая предварительно привязана к текущему состоянию и которая принимает один вход и повторно его определяет. Я обычно использовал это для получения вещей по идентификатору, отсюда и использование "reify". Мне также нравится говорить "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));
}

Это в основном то, что делает повторный выбор. Возможно, вы захотите учесть это, если планируете внедрять индивидуальное запоминание на глобальном уровне.

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 base-datum, вы можете поддерживать чистоту интерфейсов других селекторов.

Изменить: Относительно коллекций

Одна вещь, к которой вышеупомянутая реализация в настоящее время уязвима, это если extendUserDataSelectorSelector выходные данные меняются, потому что меняются его собственные селекторы зависимостей, но пользовательские данные получены userSelector не изменились, и не изменились фактические вычисленные объекты, созданные extendUserDataSelectorSelector, В этих случаях вам нужно сделать две вещи:

  1. Запоминать функцию, которая extendUserDataSelectorSelector возвращается. Я рекомендую извлечь его в отдельную глобально запоминающуюся функцию.
  2. Заворачивать someFunc так что когда он возвращает массив, он сравнивает этот массив по элементам с предыдущим результатом и, если они имеют одинаковые элементы, возвращает предыдущий результат.

Редактировать: избегая так много кэширования

Кэширование на глобальном уровне, безусловно, выполнимо, как показано выше, но этого можно избежать, если подойти к проблеме с парой других стратегий:

  1. Не срочно расширяйте данные, отложите их до каждого компонента React (или другого представления), который фактически отображает сами данные.
  2. Не с энтузиазмом конвертируйте списки идентификаторов / базовых объектов в расширенные версии, лучше, чтобы родители передавали эти идентификаторы / базовые объекты дочерним элементам.

Сначала я не следовал за ними в одном из моих главных рабочих проектов, и мне жаль, что я не имел. Как таковой, мне пришлось вместо этого пойти по пути глобального запоминания, так как это было легче исправить, чем рефакторинг всех представлений, что должно быть сделано, но у нас в настоящее время нет времени / бюджета.

Изменить 2 (или 4, я думаю?): Относительно коллекций pt. 1: Multi-Memoizing экстендер

ПРИМЕЧАНИЕ. Прежде чем пройти через эту часть, предполагается, что базовый объект, передаваемый в расширитель, будет иметь своего рода id свойство, которое может быть использовано для его уникальной идентификации, или что какое-то подобное свойство может быть получено из него дешево.

Для этого вы запоминаете сам Удлинитель, аналогично любому другому Селектору. Однако, поскольку вы хотите, чтобы расширитель запомнил свои аргументы, вы не хотите передавать ему напрямую State.

По сути, вам нужен 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
      };
    }
);

У нас есть эти две функции:

// 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 само по себе может не измениться, но могут быть сущности, на которые могут указывать идентификаторы в пределах.
  2. arrayOfSomeIds может быть новый объект в памяти, но на самом деле имеет те же идентификаторы.
  3. arrayOfSomeIds не изменилась, но коллекция, содержащая упомянутые сущности, изменилась, но конкретные сущности, на которые ссылаются эти конкретные идентификаторы, не изменились.

Вот почему я выступаю за делегирование расширения / расширения /reification/whateverelseification массивов (и других коллекций!) Настолько поздно, насколько это возможно в процессе получения-получения-представления-рендеринга: боль в миндалине иметь рассмотреть все это.

Тем не менее, это не невозможно, это просто требует дополнительной проверки.

Начиная с вышеупомянутой кэшированной версии 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, это применимо только к одному набору выходов. Скорее нам нужно использовать его для каждого основного селектора, который 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, проверка входного массива. Это выполнимо, используя реселект 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,
});

другие мысли

Тем не менее, вы должны попытаться взглянуть на то, что появляется в npm, когда вы ищете reselect а также re-reselect, Некоторые новые инструменты, которые могут или не могут быть полезны в определенных случаях. Вы можете многое сделать, просто перевыбрав и перевыбрав, а также несколько дополнительных функций в соответствии с вашими потребностями.

Проблема, с которой мы столкнулись при использовании reselect в том, что нет поддержки динамического отслеживания зависимостей. Селектор должен объявить заранее, какие части состояния вызовут повторное вычисление.

Например, у меня есть список сетевых идентификаторов пользователей и сопоставление пользователей:

{
  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 Филиал магазина:

Пример 1

Это работает, но это означает, что изменения для несвязанных пользователей (Боб, Чарли, Ева) вызовут пересчет селектора.

Я полагаю, что это проблема в выборе базового дизайна: зависимости между селекторами статичны. (Напротив, Knockout, Vue и MobX поддерживают динамические зависимости.)

Мы столкнулись с той же проблемой, и мы придумали @taskworld.com/rereselect, Вместо того, чтобы объявлять зависимости заранее и статически, зависимости собираются точно в срок и динамически во время каждого вычисления:

Пример 2

Это позволяет нашим селекторам иметь более детальный контроль над тем, какая часть состояния может вызвать пересчет селектора.

Предисловие

Я столкнулся с тем же делом, что и вы, и, к сожалению, не нашел эффективного способа вызвать селектор из тела другого селектора.

Я сказал эффективный способ, потому что у вас всегда может быть селектор ввода, который передает все состояние (хранилище), но это пересчитает ваш селектор при каждом изменении состояния:

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

подходы

Однако я обнаружил два возможных подхода для варианта использования, описанного ниже. Я думаю, что ваш случай похож, так что вы можете принять некоторые идеи.

Таким образом, дело в следующем: у вас есть селектор, который получает конкретного пользователя из хранилища по идентификатору, а селектор возвращает пользователя в определенной структуре. Скажем getUserById селектор. На данный момент все хорошо и просто, насколько это возможно. Но проблема возникает, когда вы хотите получить несколько пользователей по их идентификаторам, а также повторно использовать предыдущий селектор. Давайте назовем это getUsersByIds селектор.

1. Всегда использовать массив для ввода значений идентификаторов

Первое возможное решение состоит в том, чтобы иметь селектор, который всегда ожидает массив идентификаторов (getUsersByIds) и второй, который повторно использует предыдущий, но получит только 1 пользователя (getUserById). Поэтому, если вы хотите получить только 1 пользователя из магазина, вы должны использовать getUserById, но вы должны передать массив только с одним идентификатором пользователя.

Вот реализация:

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. является более многоразовым. Представьте себе случай, когда у нас нет идентификатора пользователя, когда мы вызываем селектор, но мы получаем его из тела селектора как отношение. В этом случае мы можем легко использовать автономную функцию. Вот псевдо-пример:

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
  }
}

Я сделал следующее обходное решение:

      const getSomeSelector = (state: RootState) => () => state.someSelector;
const getState = (state: RootState) => () => state;

const reportDerivedStepsSelector = createSelector(
    [getState, getSomeSelector],
    (getState, someSelector
) => {
    const state = getState();
    const getAnother = anotherSelector(state);
    ...
}

Функция getStateникогда не изменится, и вы можете получить полное состояние от вашего селектора, не нарушая меморандум селектора.

Recompute - это альтернатива повторному выбору, которая реализует динамическое отслеживание зависимостей и позволяет передавать любое количество аргументов селектору, вы можете проверить, решит ли это вашу проблему.

вы добавляете столько параметров, сколько хотите, и параметры могут быть другими функциями выбора.

обратный вызов конца имеет результаты этих селекторов соответственно..

      export const anySelector = createSelector(firstSelector, second, ..., (resultFromFirstSelector, resultFromSecond, ...) => { // do your thing.. });

документация

Другие вопросы по тегам