重新选择 - 调用另一个选择器的选择器?

Pat*_*kkx 11 javascript reactjs reselect

我有一个选择器:

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

yetAnotherSelector是另一个选择器,它接受用户id - id并返回一些数据.

但是,既然如此createSelector,我就无法访问它(我不希望它作为一个函数,因为memoization不会工作).

有办法在某种程度上访问商店createSelector吗?或者有没有其他方法来处理它?

编辑:

我有一个功能:

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

这样的功能正在杀死我的应用程序,导致所有内容重新渲染并让我疯狂.帮助赞赏.

!然而:

我做了一些基本的自定义memoization:

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));
}
Run Code Online (Sandbox Code Playgroud)

而且的伎俩,不过,是不是只是一个解决方法吗?

Jor*_*nev 11

前言

我遇到了与你相同的情况,遗憾的是没有找到一种从另一个选择器的身体调用选择器的有效方法.

我说有效的方法,因为你总是可以有一个输入选择器,它传递整个状态(存储),但这会在每个状态的变化上重新计算你的选择器:

const someSelector = createSelector(
   getUserIdsSelector,
   state => state,
   (ids, state) => ids.map((id) => yetAnotherSelector(state, id)
)
Run Code Online (Sandbox Code Playgroud)

途径

但是,我发现了两种可能的方法,用于下面描述的用例.我想你的情况类似,所以你可以采取一些见解.

所以情况如下:你有一个选择器,它通过id从Store获取特定用户,并且选择器返回特定结构中的User.让我们说getUserById选择器.现在一切都很好,很简单.但是当您希望通过其ID获取多个用户并且还重用以前的选择器时,会出现问题.我们将它命名为getUsersByIds选择器.

1.始终使用数组,输入id值

第一种可能的解决方案是让一个总是需要一个id(getUsersByIds)数组的选择器和一个重用前一个的数组,但它只能得到1个User(getUserById).因此,当您只想从商店获得1个用户时,您必须使用getUserById,但您必须传递一个只有一个用户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])
Run Code Online (Sandbox Code Playgroud)

用法:

// 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] }) 
Run Code Online (Sandbox Code Playgroud)

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]})
Run Code Online (Sandbox Code Playgroud)

用法:

// 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] }) 
Run Code Online (Sandbox Code Playgroud)

结论

方法#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
  }
}
Run Code Online (Sandbox Code Playgroud)


Tha*_*hai 8

我们使用时所面临的一个问题reselect是,有动态的依赖性跟踪的支持。选择器需要提前声明状态的哪些部分将导致重新计算。

例如,我有一个在线用户ID列表,以及一个用户映射:

{
  onlineUserIds: [ 'alice', 'dave' ],
  notifications: [ /* unrelated data */ ]
  users: {
    alice: { name: 'Alice' },
    bob: { name: 'Bob' },
    charlie: { name: 'Charlie' },
    dave: { name: 'Dave' },
    eve: { name: 'Eve' }
  }
}
Run Code Online (Sandbox Code Playgroud)

我想选择一个在线用户列表,例如[ { name: 'Alice' }, { name: 'Dave' } ]

由于我无法预先知道哪些用户将在线,所以我需要声明state.users对商店整个分支的依赖:

例子1

这可行,但是这意味着对无关用户(bob,charlie,eve)的更改将导致重新计算选择器。

我认为这是reselect的基本设计选择中的一个问题:选择器之间的依赖关系是静态的。(相比之下,Knockout,Vue和MobX确实支持动态依赖项。)

我们面临同样的问题,我们想到了@taskworld.com/rereselect。无需预先声明和静态声明依赖关系,而是在每次计算过程中实时并动态地收集依赖关系:

例子2

这使我们的选择器可以更精细地控制状态的哪一部分可以导致重新计算选择器。


Jos*_*ski 7

对于您的someFunc案例

对于您的特定情况,我将创建一个选择器,该选择器本身将返回扩展器。

也就是说,为此:

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

我会写:

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
      };
    }
);
Run Code Online (Sandbox Code Playgroud)

然后someFunc将变为:

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

我称其为reifier模式,因为它创建了一个预先绑定到当前状态的函数,该函数接受单个输入并对其进行调整。我通常将其用于通过id获取信息,因此使用了“ reify”。我也喜欢说“ 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));
}
Run Code Online (Sandbox Code Playgroud)

基本上,这就是重新选择的功能。您不妨考虑一下,如果您计划在全球范围内实施按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);
Run Code Online (Sandbox Code Playgroud)

或者,您也可以只用一个蝴蝶结包装您的备忘的多重选择器创建者,并将其命名为createCachedSelector,因为它基本上是同一回事。

编辑:为什么返回函数

可以执行此操作的另一种方法是只选择运行extendUserDataSelector计算所需的所有适当数据,但这意味着将要使用该计算的所有其他函数暴露给其接口。通过返回仅接受单个user基本数据的函数,可以使其他选择器的界面保持干净。

编辑:关于收藏

上面的实现当前容易受到影响的一件事是if extendUserDataSelectorSelector的输出发生了变化,因为其自身的依存选择器发生了变化,但是获得的用户数据userSelector没有发生变化,并且所创建的实际计算实体也没有发生变化extendUserDataSelectorSelector。在这种情况下,您需要做两件事:

  1. 多次记忆extendUserDataSelectorSelector返回的函数。我建议将其提取到单独的全局存储函数中。
  2. 包装,someFunc以便当它返回数组时,将按元素将该数组与上一个结果进行比较,如果它们具有相同的元素,则返回上一个结果。

编辑:避免太多缓存

如上所示,在全局级别进行缓存当然是可行的,但是如果您考虑其他几个策略来解决问题,则可以避免这种情况:

  1. 不要急于扩展数据,将其推迟到实际呈现数据本身的每个React(或其他视图)组件中。
  2. 不要急于将ID /基础对象的列表转换为扩展版本,而是让父母将这些ID /基础对象传递给孩子。

一开始我没有遵循那些主要工作项目中的内容,但愿我有。实际上,我后来不得不走全球记忆路线,因为这比重构所有视图更容易解决,这是应该做的,但我们目前没有时间/预算。

编辑2(或者我猜是4?):关于集合pt。1:多次记忆扩展器

注意:在进行本部分之前,假定要传递给扩展器的基础实体将具有某种id可以用来唯一标识它的属性,或者可以廉价地从中获得某种相似的属性。

为此,您可以像其他选择器一样记住扩展器本身。但是,由于您希望扩展程序记住其参数,因此您不想直接将State传递给它。

基本上,您需要一个Multi-Memoizer,它的作用方式基本上与重新选择 Selector的方式相同。实际上,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);
}
Run Code Online (Sandbox Code Playgroud)

然后代替旧的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
      };
    }
);
Run Code Online (Sandbox Code Playgroud)

我们具有以下两个功能:

// 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
    )
);
Run Code Online (Sandbox Code Playgroud)

extendUserData是真正的缓存发生的地方,尽管有合理的警告:如果您有很多baseUser实体,它可能会变得很大。

编辑2(或者我猜是4?):关于集合pt。2:数组

数组是缓存存在的祸根:

  1. arrayOfSomeIds 本身可能不会更改,但是指向其中的id可能具有的实体。
  2. arrayOfSomeIds 可能是内存中的一个新对象,但实际上具有相同的ID。
  3. arrayOfSomeIds 并没有改变,但是包含引用实体的集合确实发生了改变,但是这些特定ID所引用的特定实体没有改变。

这就是为什么我主张将数组(和其他集合!)的扩展/扩展/定义/任何形式的委派委托到尽可能晚的数据获取-派生视图呈现过程的原因:杏仁核的痛苦考虑所有这些。

也就是说,这并非不可能,只会带来一些额外的检查。

从上述的缓存版本开始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);
Run Code Online (Sandbox Code Playgroud)

然后,我们可以将其包装在另一个仅缓存输出的函数中:

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;
}
Run Code Online (Sandbox Code Playgroud)

现在,我们不能仅将其应用于的结果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)),
  }
)
Run Code Online (Sandbox Code Playgroud)

奖励部分:数组输入

您可能已经注意到,我们只检查数组输出,涉及情况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)
);
Run Code Online (Sandbox Code Playgroud)

这将进一步更改someFunc定义,尽管只需更改selectorCreator

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

其他想法

综上所述,您应该尝试查看在搜索reselect和时npm中显示的内容re-reselect。那里的一些新工具可能对某些情况有用,也可能没有用。不过,您可以做很多事情,只需重新选择和重新选择,再加上一些满足您需要的额外功能。

  • 这里有点晚了,但我认为@JosephSikorski 提出的解决方案的真正聪明之处主要归结为通过中间步骤绑定商店的想法。这解决了子选择器无法访问状态的问题,同时也不会导致缓存破坏问题,因为返回的函数签名应该保持静态。关于这个答案唯一不幸的部分是我认为可能有_太多_细节,并且提出的简单解决方案有点丢失。除此之外,非常感谢您提供的好主意,我将继续将其作为“reifier”模式进行传播! (2认同)