État en tant que tableau d'objets vs objet indexé par id

94

Dans le chapitre sur la conception de la forme de l'état , la documentation suggère de conserver votre état dans un objet indexé par ID:

Conservez chaque entité d'un objet stockée avec un ID comme clé et utilisez des ID pour la référencer à partir d'autres entités ou listes.

Ils continuent à déclarer

Considérez l'état de l'application comme une base de données.

Je travaille sur la forme de l'état pour une liste de filtres, dont certains seront ouverts (ils sont affichés dans une fenêtre contextuelle), ou ont des options sélectionnées. Quand j'ai lu «Pensez à l'état de l'application comme une base de données», j'ai pensé à les considérer comme une réponse JSON car elle serait renvoyée par une API (elle-même soutenue par une base de données).

Alors je pensais à ça comme

[{
    id: '1',
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  {
    id: '10',
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }]

Cependant, la documentation suggère un format plus semblable à

{
   1: { 
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  10: {
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }
}

En théorie, cela ne devrait pas avoir d'importance tant que les données sont sérialisables (sous la rubrique «État») .

J'ai donc opté pour l'approche tableau d'objets avec bonheur, jusqu'à ce que j'écrive mon réducteur.

Avec l'approche objet par identifiant (et l'utilisation libérale de la syntaxe de diffusion), la OPEN_FILTERpartie du réducteur devient

switch (action.type) {
  case OPEN_FILTER: {
    return { ...state, { ...state[action.id], open: true } }
  }

Alors qu'avec l'approche tableau d'objets, c'est la plus verbeuse (et dépendante de la fonction d'assistance)

switch (action.type) {
   case OPEN_FILTER: {
      // relies on getFilterById helper function
      const filter = getFilterById(state, action.id);
      const index = state.indexOf(filter);
      return state
        .slice(0, index)
        .concat([{ ...filter, open: true }])
        .concat(state.slice(index + 1));
    }
    ...

Mes questions sont donc triples:

1) La simplicité du réducteur est-elle la motivation pour aller avec l'approche objet-clé-par-id? Y a-t-il d'autres avantages à cette forme d'État?

et

2) Il semble que l'approche par identifiant d'objet rend plus difficile le traitement des entrées / sorties JSON standard pour une API. (C'est pourquoi j'ai opté pour le tableau d'objets en premier lieu.) Donc, si vous optez pour cette approche, utilisez-vous simplement une fonction pour le transformer d'avant en arrière entre le format JSON et le format de forme d'état? Cela semble maladroit. (Bien que si vous préconisez cette approche, cela fait-il partie de votre raisonnement selon lequel c'est moins maladroit que le réducteur de tableau d'objets ci-dessus?)

et

3) Je sais que Dan Abramov a conçu le redux pour être théoriquement indépendant de la structure des données d'état (comme suggéré par "Par convention, l'état de niveau supérieur est un objet ou une autre collection de valeurs-clés comme une carte, mais techniquement, il peut être n'importe quel type , " je souligne). Mais compte tenu de ce qui précède, est-il simplement "recommandé" de le conserver comme un objet associé à son ID, ou y a-t-il d'autres problèmes imprévus que je vais rencontrer en utilisant un tableau d'objets qui le rendent tel que je devrais simplement abandonner cela planifier et essayer de rester avec un objet saisi par ID?

nickcoxdotme
la source
2
C'est une question intéressante, et j'ai eue aussi, juste pour donner un aperçu, bien que j'ai tendance à normaliser en redux au lieu de tableaux (simplement parce que la recherche est plus facile), je trouve que si vous prenez l'approche normalisée, le tri devient un problème parce que vous n'obtenez pas la même structure que le tableau, vous êtes donc obligé de vous trier.
Robert Saunders
Je vois un problème dans l'approche 'objet-clé-par-id', mais ce n'est pas fréquent, mais nous devons considérer ce cas lors de l'écriture d'une application d'interface utilisateur. Alors que faire si je veux changer l'ordre de l'entité à l'aide d'un élément glisser-déposer répertorié comme liste ordonnée? Habituellement, l'approche «objet-clé-par-id» échoue ici et j'irais sûrement avec un éventail d'approche objet pour éviter des problèmes aussi généreux. Il pourrait y avoir plus mais pensé à partager ceci ici
Kunal Navhate
Comment trier un objet composé d'objets? Cela semble impossible.
David Vielhuber
@DavidVielhuber Vous voulez dire en plus d'utiliser quelque chose comme le lodash sort_by? const sorted = _.sortBy(collection, 'attribute');
nickcoxdotme
Oui. Actuellement, nous convertissons ces objets en tableaux à l'intérieur d'une propriété de vue calculée
David Vielhuber

Réponses:

46

Q1: La simplicité du réducteur est le résultat de ne pas avoir à chercher dans le tableau pour trouver la bonne entrée. L'avantage est de ne pas avoir à chercher dans le tableau. Les sélecteurs et autres accesseurs de données peuvent accéder et accèdent souvent à ces éléments par id. Le fait de devoir rechercher dans la baie pour chaque accès devient un problème de performances. Lorsque vos baies deviennent plus volumineuses, le problème de performances s'aggrave fortement. De plus, à mesure que votre application devient plus complexe, affichant et filtrant les données à plus d'endroits, le problème s'aggrave également. La combinaison peut être préjudiciable. En accédant aux éléments par id, le temps d'accès passe de O(n)à O(1), ce qui nfait une énorme différence pour les éléments volumineux (ici les éléments de tableau).

Q2: Vous pouvez utiliser normalizrpour vous aider à convertir de l'API en magasin. Depuis normalizr V3.1.0, vous pouvez utiliser denormalize pour aller dans l'autre sens. Cela dit, les applications sont souvent plus de consommateurs que de producteurs de données et, à ce titre, la conversion en magasin est généralement effectuée plus fréquemment.

Q3: Les problèmes que vous rencontrerez en utilisant une baie ne sont pas tant des problèmes de convention de stockage et / ou d'incompatibilités, mais davantage de problèmes de performances.

DDS
la source
le normalisateur est à nouveau la chose qui créerait sûrement de la douleur une fois que nous changerons les defs dans le backend. Donc, cela doit rester à jour à chaque fois
Kunal Navhate
12

Considérez l'état de l'application comme une base de données.

C'est l'idée clé.

1) Avoir des objets avec des identifiants uniques vous permet de toujours utiliser cet identifiant lors du référencement de l'objet, vous devez donc passer le minimum de données entre les actions et les réducteurs. C'est plus efficace que d'utiliser array.find (...). Si vous utilisez l'approche par tableau, vous devez passer l'objet entier et cela peut devenir désordonné très bientôt, vous pourriez finir par recréer l'objet sur différents réducteurs, actions, ou même dans le conteneur (vous ne le voulez pas). Les vues pourront toujours obtenir l'objet complet même si leur réducteur associé ne contient que l'ID, car lors du mappage de l'état, vous obtiendrez la collection quelque part (la vue obtient l'état complet pour le mapper aux propriétés). En raison de tout ce que j'ai dit, les actions finissent par avoir le minimum de paramètres, et réduisent la quantité minimale d'informations, essayez-le,

2) La connexion à l'API ne doit pas affecter l'architecture de votre stockage et des réducteurs, c'est pourquoi vous avez des actions, pour garder la séparation des soucis. Mettez simplement votre logique de conversion dans et hors de l'API dans un module réutilisable, importez ce module dans les actions qui utilisent l'API, et cela devrait être tout.

3) J'ai utilisé des tableaux pour les structures avec des identifiants, et voici les conséquences imprévues que j'ai subies:

  • Recréer constamment des objets tout au long du code
  • Transmettre des informations inutiles aux réducteurs et aux actions
  • En conséquence, du code mauvais, pas propre et non évolutif.

J'ai fini par changer ma structure de données et réécrire beaucoup de code. Vous avez été prévenu, ne vous mettez pas en difficulté.

Aussi:

4) La plupart des collections avec des identifiants sont destinées à utiliser l'identifiant comme référence à l'objet entier, vous devriez en profiter. Les appels API recevront l'ID , puis le reste des paramètres, ainsi que vos actions et vos réducteurs.

Marco Scabbiolo
la source
Je rencontre un problème où nous avons une application avec beaucoup de données (1000 à 10000) stockées par identifiant dans un objet du magasin redux. Dans les vues, ils utilisent tous des tableaux triés pour afficher des données de séries chronologiques. Cela signifie qu'à chaque fois qu'un rendu est effectué, il doit prendre tout l'objet, le convertir en tableau et le trier. J'ai été chargé d'améliorer les performances de l'application. S'agit-il d'un cas d'utilisation où il est plus logique de stocker vos données dans un tableau trié et d'utiliser la recherche binaire pour effectuer des suppressions et des mises à jour au lieu d'un objet?
William Chou
J'ai fini par devoir créer d'autres cartes de hachage dérivées de ces données pour minimiser le temps de calcul des mises à jour. Cela fait que la mise à jour de toutes les différentes vues nécessite leur propre logique de mise à jour. Avant cela, tous les composants prenaient l'objet du magasin et reconstruisaient les structures de données dont il avait besoin pour créer sa vue. La seule façon dont je peux penser pour assurer une secousse minimale dans l'interface utilisateur est d'utiliser un travailleur Web pour effectuer la conversion d'objet en tableau. Le compromis pour cela est une logique de récupération et de mise à jour plus simple, car tous les composants ne dépendent que d'un type de données à lire et à écrire.
William Chou
8

1) La simplicité du réducteur est-elle la motivation pour aller avec l'approche objet-clé-par-id? Y a-t-il d'autres avantages à cette forme d'État?

La principale raison pour laquelle vous souhaitez conserver les entités dans les objets stockés avec des ID sous forme de clés (également appelées normalisées ), est qu'il est vraiment fastidieux de travailler avec des objets profondément imbriqués (ce que vous obtenez généralement des API REST dans une application plus complexe) - à la fois pour vos composants et vos réducteurs.

Il est un peu difficile d'illustrer les avantages d'un état normalisé avec votre exemple actuel (car vous n'avez pas de structure profondément imbriquée ). Mais disons que les options (dans votre exemple) avaient également un titre et ont été créées par les utilisateurs de votre système. Cela donnerait à la réponse quelque chose comme ceci à la place:

[{
  id: 1,
  name: 'View',
  open: false,
  options: [
    {
      id: 10, 
      title: 'Option 10',
      created_by: { 
        id: 1, 
        username: 'thierry' 
      }
    },
    {
      id: 11, 
      title: 'Option 11',
      created_by: { 
        id: 2, 
        username: 'dennis'
      }
    },
    ...
  ],
  selectedOption: ['10'],
  parent: null,
},
...
]

Supposons maintenant que vous vouliez créer un composant qui affiche une liste de tous les utilisateurs qui ont créé des options. Pour ce faire, vous devez d'abord demander tous les éléments, puis parcourir chacune de leurs options et enfin obtenir le created_by.username.

Une meilleure solution serait de normaliser la réponse en:

results: [1],
entities: {
  filterItems: {
    1: {
      id: 1,
      name: 'View',
      open: false,
      options: [10, 11],
      selectedOption: [10],
      parent: null
    }
  },
  options: {
    10: {
      id: 10,
      title: 'Option 10',
      created_by: 1
    },
    11: {
      id: 11,
      title: 'Option 11',
      created_by: 2
    }
  },
  optionCreators: {
    1: {
      id: 1,
      username: 'thierry',
    },
    2: {
      id: 2,
      username: 'dennis'
    }
  }
}

Avec cette structure, il est beaucoup plus facile et plus efficace de lister tous les utilisateurs qui ont créé des options (nous les avons isolés dans entity.optionCreators, il suffit donc de parcourir cette liste).

Il est également assez simple d'afficher par exemple les noms d'utilisateur de ceux qui ont créé des options pour l'élément de filtre avec l'ID 1:

entities
  .filterItems[1].options
  .map(id => entities.options[id])
  .map(option => entities.optionCreators[option.created_by].username)

2) Il semble que l'approche par identifiant d'objet rend plus difficile le traitement des entrées / sorties JSON standard pour une API. (C'est pourquoi j'ai opté pour le tableau d'objets en premier lieu.) Donc, si vous optez pour cette approche, utilisez-vous simplement une fonction pour le transformer d'avant en arrière entre le format JSON et le format de forme d'état? Cela semble maladroit. (Bien que si vous préconisez cette approche, cela fait-il partie de votre raisonnement selon lequel c'est moins maladroit que le réducteur de tableau d'objets ci-dessus?)

Une réponse JSON peut être normalisée en utilisant par exemple normalizr .

3) Je sais que Dan Abramov a conçu le redux pour être théoriquement indépendant de la structure des données d'état (comme suggéré par "Par convention, l'état de niveau supérieur est un objet ou une autre collection de valeurs-clés comme une carte, mais techniquement, il peut être n'importe quel type, "je souligne). Mais compte tenu de ce qui précède, est-il simplement "recommandé" de le conserver comme un objet associé à son ID, ou y a-t-il d'autres problèmes imprévus que je vais rencontrer en utilisant un tableau d'objets qui le rendent tel que je devrais simplement abandonner cela planifier et essayer de rester avec un objet saisi par ID?

C'est probablement une recommandation pour des applications plus complexes avec de nombreuses réponses d'API profondément imbriquées. Dans votre exemple particulier, cela n'a pas vraiment d'importance.

tobiasandersen
la source
1
maprenvoie undefined comme ici , si les ressources sont extraites séparément, ce qui rend filtertrop compliqué. Y a-t-il une solution?
Saravanabalagi Ramachandran
1
@tobiasandersen pensez-vous qu'il est correct pour le serveur de renvoyer des données normalisées idéales pour react / redux, pour éviter que le client fasse la conversion via des bibliothèques comme normalizr? En d'autres termes, faites en sorte que le serveur normalise les données et non le client.
Matthieu