web-dev-qa-db-fra.com

Resélectionnez - sélecteur qui appelle un autre sélecteur?

J'ai un sélecteur:

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

Ce yetAnotherSelector est un autre sélecteur, qui prend l'ID utilisateur - id et renvoie des données.

Cependant, puisque c'est createSelector, je n'ai pas accès à la stocker dedans (je ne le veux pas en tant que fonction car la mémorisation ne fonctionnerait pas alors).

Existe-t-il un moyen d'accéder au magasin d'une manière ou d'une autre à l'intérieur de createSelector? Ou existe-t-il un autre moyen de le gérer?

MODIFIER :

J'ai une fonction:

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

Une telle fonction tue mon application, provoquant tout rendu et me rendant fou. Aide appréciée.

!! Pourtant:

J'ai fait une mémorisation de base et personnalisée:

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

Et c'est le cas l'astuce, mais n'est-ce pas juste une solution de contournement?

9
Patrickkx

Préface

J'ai été confronté au même cas que le vôtre et je n'ai malheureusement pas trouvé un moyen efficace d'appeler un sélecteur à partir du corps d'un autre sélecteur.

J'ai dit de manière efficace , car vous pouvez toujours avoir un sélecteur d'entrée, qui transmet tout l'état (stocker), mais cela recalculera votre sélecteur sur chaque changements d'état:

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

Approches

Cependant, j'ai découvert deux approches possibles, pour le cas d'utilisation décrit ci-dessous. Je suppose que votre cas est similaire, vous pouvez donc en tirer quelques informations.

Ainsi, le cas est le suivant: vous avez un sélecteur, qui obtient un utilisateur spécifique du magasin par un identifiant, et le sélecteur renvoie l'utilisateur dans une structure spécifique. Disons que le sélecteur getUserById. Pour l'instant, tout est aussi simple que possible. Mais le problème se produit lorsque vous souhaitez obtenir plusieurs utilisateurs par leur identifiant et réutiliser également le sélecteur précédent. Appelons-le getUsersByIds sélecteur.

1. Utiliser toujours un tableau pour les valeurs des ID d'entrée

La première solution possible est d'avoir un sélecteur qui attend toujours un tableau d'ID (getUsersByIds) et un second, qui réutilise le précédent, mais il n'obtiendra qu'un seul utilisateur (getUserById). Ainsi, lorsque vous souhaitez obtenir un seul utilisateur du magasin, vous devez utiliser getUserById, mais vous devez transmettre un tableau avec un seul ID utilisateur.

Voici l'implémentation:

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])

tilisation:

// 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. Réutiliser le corps du sélecteur, en tant que fonction autonome

L'idée ici est de séparer la partie commune et réutilisable du corps du sélecteur dans une fonction autonome, afin que cette fonction puisse être appelée à partir de tous les autres sélecteurs.

Voici l'implémentation:

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

tilisation:

// 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] }) 

Conclusion

Approche # 1. a moins de passe-partout (nous n'avons pas de fonction autonome) et a une implémentation propre.

Approche # 2. est plus réutilisable. Imaginez le cas, où nous n'avons pas d'ID utilisateur lorsque nous appelons un sélecteur, mais nous l'obtenons du corps du sélecteur en tant que relation. Dans ce cas, nous pouvons facilement réutiliser la fonction autonome. Voici un pseudo exemple:

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

Un problème rencontré lors de l'utilisation de reselect est que il n'y a pas de support pour le suivi dynamique des dépendances. Un sélecteur doit déclarer à l'avance quelles parties de l'état provoquera un recalcul.

Par exemple, j'ai une liste d'ID utilisateur en ligne et un mappage d'utilisateurs:

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

Je souhaite sélectionner une liste d'utilisateurs en ligne, par exemple [ { name: 'Alice' }, { name: 'Dave' } ].

Comme je ne peux pas savoir à l'avance quels utilisateurs seront en ligne, je dois déclarer une dépendance à toute la branche state.users De la boutique:

Example 1

Cela fonctionne, mais cela signifie que les modifications apportées aux utilisateurs non liés (bob, charlie, eve) entraîneront le recalcul du sélecteur.

Je crois que c'est un problème dans le choix de conception fondamental de reselect: les dépendances entre les sélecteurs sont statiques. (En revanche, Knockout, Vue et MobX font prendre en charge les dépendances dynamiques.)

Nous avons rencontré le même problème et nous avons trouvé @taskworld.com/rereselect . Au lieu de déclarer les dépendances à l'avance et de manière statique, les dépendances sont collectées juste à temps et dynamiquement pendant chaque calcul:

Example 2

Cela permet à nos sélecteurs d'avoir un contrôle plus précis de la partie de l'état qui peut entraîner le recalcul d'un sélecteur.

8
Thai

Pour votre cas someFunc

Pour votre cas spécifique, je créerais un sélecteur qui renvoie lui-même un répéteur.

Autrement dit, pour cela:

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

Je souhaiterai écrire:

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 deviendrait alors:

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

Je l'appelle le modèle de réification car il crée une fonction qui est pré-liée à l'état actuel et qui accepte une seule entrée et la réifie. Je l'utilisais généralement pour récupérer les choses par identifiant, d'où l'utilisation de "reify". J'aime aussi dire "réifier", qui est honnêtement la principale raison pour laquelle je l'appelle ainsi.

Pour votre cas cependant

Dans ce cas:

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

C'est essentiellement ce que fait re-reselect . Vous voudrez peut-être considérer cela si vous prévoyez d'implémenter la mémorisation par identifiant au niveau mondial.

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);

Ou vous pouvez simplement conclure votre créateur multi-sélecteur mémorisé avec un arc et l'appeler createCachedSelector, car c'est essentiellement la même chose.

Modifier: Pourquoi retourner des fonctions

Une autre façon de le faire est de sélectionner simplement toutes les données appropriées nécessaires pour exécuter le calcul extendUserDataSelector, mais cela signifie exposer toutes les autres fonctions qui souhaitent utiliser ce calcul à son interface. En renvoyant une fonction qui n'accepte qu'une seule base de données user, vous pouvez garder les interfaces des autres sélecteurs propres.

Edit: Concernant les collections

L'implémentation ci-dessus est actuellement vulnérable si la sortie de extendUserDataSelectorSelector change parce que ses propres sélecteurs de dépendance changent, mais les données utilisateur obtenues par userSelector n'ont pas changé, et les entités calculées réelles n'ont pas non plus été créées par extendUserDataSelectorSelector. Dans ces cas, vous devrez faire deux choses:

  1. Mémorisez plusieurs fois la fonction que extendUserDataSelectorSelector renvoie. Je recommande de l'extraire dans une fonction séparée mémorisée globalement.
  2. Enveloppez someFunc afin que lorsqu'il retourne un tableau, il compare ce tableau élément par élément au résultat précédent, et s'ils ont les mêmes éléments, renvoie le résultat précédent.

Edit: éviter autant de mise en cache

La mise en cache au niveau mondial est certainement faisable, comme indiqué ci-dessus, mais vous pouvez éviter cela si vous abordez le problème avec quelques autres stratégies à l'esprit:

  1. N'étendez pas les données avec impatience, reportez-les à chaque composant React (ou autre vue) qui rend les données elles-mêmes.
  2. Ne convertissez pas avec impatience les listes d'identifiants/objets de base en versions étendues, faites plutôt que les parents transmettent ces identifiants/objets de base aux enfants.

Je ne les ai pas suivis au début dans l'un de mes grands projets de travail, et j'aurais bien aimé. Dans l'état actuel des choses, j'ai dû emprunter la voie de la mémorisation globale plus tard car c'était plus facile à corriger que de refactoriser toutes les vues, quelque chose qui devrait être fait mais pour lequel nous manquons actuellement de temps/budget.

Edit 2 (ou 4 je suppose?): Re-Regarding Collections pt. 1: Mémorisation multiple de l'extension

Remarque: avant de passer par cette partie, il suppose que l'entité de base transmise à l'extendeur aura une sorte de propriété id qui peut être utilisée pour l'identifier de manière unique, ou qu'une sorte de propriété similaire peut être dérivé à bon marché.

Pour cela, vous mémorisez l'Extender lui-même, d'une manière similaire à tout autre sélecteur. Cependant, comme vous voulez que l'Extender mémorise ses arguments, vous ne voulez pas lui passer directement State.

Fondamentalement, vous avez besoin d'un Multi-Memoizer qui agit essentiellement de la même manière que re-reselect pour les sélecteurs. En fait, il est trivial de poinçonner createCachedSelector pour le faire pour nous:

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

Ensuite, au lieu de l'ancien 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
      };
    }
);

Nous avons ces deux fonctions:

// 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
    )
);

Ce extendUserData est l'endroit où la mise en cache réelle se produit, bien que ce soit un avertissement: si vous avez beaucoup d'entités baseUser, elle pourrait devenir assez grande.

Edit 2 (ou 4 je suppose?): Re-Regarding Collections pt. 2: Tableaux

Les tableaux sont le fléau de la mise en cache de l'existence:

  1. arrayOfSomeIds peut lui-même ne pas changer, mais les entités vers lesquelles les identifiants pointent pourraient avoir.
  2. arrayOfSomeIds peut être un nouvel objet en mémoire, mais en réalité a les mêmes identifiants.
  3. arrayOfSomeIds n'a pas changé, mais la collection contenant les entités auxquelles il est fait référence a changé, mais les entités particulières auxquelles se réfèrent ces identifiants spécifiques n'ont pas changé.

C'est tout pour cela que je plaide pour la délégation de l'extension/expansion/réification/quelle que soit l'identification des tableaux (et d'autres collections!) Au plus tard dans le processus d'obtention de données-dérivation-vue-rendu possible: c'est une douleur dans l'amygdale d'avoir de considérer tout cela.

Cela dit, ce n'est pas impossible, cela entraîne simplement des vérifications supplémentaires.

En commençant par la version en cache ci-dessus de 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);

Nous pouvons ensuite l'envelopper dans une autre fonction qui met simplement en cache la sortie:

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

Maintenant, nous ne pouvons pas simplement appliquer cela au résultat de createCachedSelector, qui ne s'appliquerait qu'à un seul ensemble de sorties. Nous devons plutôt l'utiliser pour chaque sélecteur sous-jacent créé par createCachedSelector. Heureusement, re-reselect vous permet de configurer le créateur de sélecteur qu'il utilise:

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

Partie bonus: entrées de tableau

Vous avez peut-être remarqué que nous vérifions uniquement les sorties de tableau, couvrant les cas 1 et 3, ce qui peut être suffisant. Parfois, cependant, vous pouvez également avoir besoin du cas de capture 2 pour vérifier le tableau d'entrée. Ceci est possible en utilisant createSelectorCreator à resélectionner créer notre propre createSelector en utilisant une fonction d'égalité personnalisée

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)
);

Cela modifie encore la définition de someFunc, mais simplement en changeant la selectorCreator:

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

D'autres pensées

Cela dit, vous devriez essayer de voir ce qui apparaît dans npm lorsque vous recherchez reselect et re-reselect. Quelques nouveaux outils là-bas qui peuvent ou non être utiles dans certains cas. Cependant, vous pouvez faire beaucoup avec juste resélectionner et resélectionner plus quelques fonctions supplémentaires pour répondre à vos besoins.

7
Joseph Sikorski