Dans le chapitre sur Conception de la forme d'état , la documentation vous suggère de conserver votre état dans un objet associé à un ID:
Conservez chaque entité dans un objet stocké avec un ID en tant que clé et utilisez des ID pour la référencer à partir d'autres entités ou listes.
Ils continuent à dire
Considérez l’état de l’application comme une base de données.
Je travaille sur la forme d'état pour obtenir 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 telle qu’elle serait renvoyée par une API (elle-même adossée à une base de données).
Donc je pensais à ça
[{
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 proche de
{
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") .
Je suis donc allé avec plaisir à l'approche du tableau d'objets, jusqu'à ce que j'écrive mon réducteur.
Avec l’approche object-by-by-id (et l’utilisation généralisée de la syntaxe spread), le OPEN_FILTER
une partie 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 la fonction d'assistance dépendante)
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));
}
...
1) La simplicité du réducteur est-elle la motivation pour adopter l'approche par clé d'objet par identité? Existe-t-il d'autres avantages à cette forme d'état?
et
2) Il semble que l'approche par clé d'identification par objet rend plus difficile le traitement des entrées/sorties JSON standard pour une API. (C’est pourquoi j’ai tout d’abord choisi le tableau d’objets.) Si vous optez pour cette approche, utilisez-vous simplement une fonction pour la transformer en format JSON et en format de forme d’état? Cela semble maladroit. (Bien que si vous préconisez cette approche, votre raisonnement est-il moins clair que le réducteur de la matrice d'objets ci-dessus?)
et
3) Je sais que Dan Abramov a conçu redux pour être théoriquement agnostique sur la structure de 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 telle qu'une carte, mais techniquement, il peut s'agir de n'importe quel type , " l'emphase mienne). Mais étant donné ce qui précède, est-il simplement "recommandé" de conserver un objet identifié par ID, ou y a-t-il d'autres points de douleur imprévus que je vais rencontrer en utilisant un tableau d'objets qui le rend tel que je devrais simplement abandonner planifier et essayer de s'en tenir à un objet identifié par ID?
Q1: La simplicité du réducteur résulte de l’absence de recherche dans le tableau pour trouver la bonne entrée. Ne pas avoir à chercher dans le tableau est l'avantage. Les sélecteurs et autres accesseurs de données peuvent souvent accéder à ces éléments par id
. Devoir rechercher dans le tableau pour chaque accès devient un problème de performances. Lorsque vos baies deviennent plus grandes, le problème de performances s’aggrave. En outre, à mesure que votre application devient plus complexe, affichant et filtrant les données dans 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, pour les grands n
(ici les éléments de tableau) fait une énorme différence.
Q2: Vous pouvez utiliser normalizr
pour vous aider avec la conversion de l'API à stocker. A partir de la version 3.1.0 de normalizr, vous pouvez utiliser denormalize pour aller dans l'autre sens. Cela dit, les applications sont souvent plus des consommateurs que des producteurs de données et, par conséquent, la conversion en magasin est généralement plus fréquente.
Q3: Les problèmes que vous rencontrez en utilisant un tableau ne concernent pas tant la convention de stockage et/ou les incompatibilités, mais davantage de problèmes de performances.
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 lorsque vous le référencez. Vous devez donc transmettre 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 de tableau, vous devez passer l'objet entier et que cela peut devenir très désordonné, vous pourriez éventuellement recréer l'objet sur différents réducteurs, actions ou même dans le conteneur (vous ne le souhaitez 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 un minimum de paramètres et réduisent le minimum d'informations. Essayez-le, essayez les deux méthodes et vous verrez que l'architecture est plus évolutive et plus propre avec ID si les collections ont un ID.
2) La connexion à l’API ne doit pas affecter l’architecture de votre stockage et de vos réducteurs, c’est pourquoi vous avez des actions à prendre pour maintenir la séparation des problèmes. Insérez simplement votre logique de conversion dans l'API d'un module réutilisable et importez-le dans les actions qui utilisent l'API, et ce devrait être le cas.
3) J'ai utilisé des tableaux pour les structures avec ID, et ce sont les conséquences imprévues que j'ai subies:
J'ai fini par changer ma structure de données et réécrire beaucoup de code. Vous avez été prévenu, s'il vous plait, ne vous faites pas de problèmes.
Aussi:
4) La plupart des collections avec ID sont censées utiliser l'ID comme référence à l'objet entier. Vous devez en tirer parti. Les appels d'API recevront l'ID et ensuite le reste des paramètres, ainsi que vos actions et vos réducteurs.
1) La simplicité du réducteur est-elle la motivation pour adopter l'approche par clé d'objet par identité? Existe-t-il d'autres avantages à cette forme d'état?
La principale raison pour laquelle vous souhaitez conserver les entités dans des objets stockés avec des identifiants sous forme de clés (également appelées normalisées ) est que le travail avec objets profondément imbriqués (ce que vous obtenez généralement de 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 imbriquée profondément ). Mais disons que les options (dans votre exemple) ont également un titre et ont été créées par les utilisateurs de votre système. La réponse ressemblerait plutôt à ceci:
[{
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 ayant créé des options. Pour ce faire, vous devez d'abord demander tous les éléments, puis parcourir chacune de leurs options, puis 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 répertorier tous les utilisateurs qui ont créé des options (nous les avons isolés dans entity.optionCreators, nous devons donc parcourir cette liste en boucle).
C'est aussi assez simple de montrer 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 clé d'identification par objet rend plus difficile le traitement des entrées/sorties JSON standard pour une API. (C’est pourquoi j’ai tout d’abord choisi le tableau d’objets.) Si vous optez pour cette approche, utilisez-vous simplement une fonction pour la transformer en format JSON et en format de forme d’état? Cela semble maladroit. (Bien que si vous préconisez cette approche, votre raisonnement est-il moins clair que le réducteur de la matrice 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 redux pour être théoriquement agnostique sur la structure de 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, tapez ", soulignez le mien). Mais étant donné ce qui précède, est-il simplement "recommandé" de conserver un objet identifié par ID, ou y a-t-il d'autres points de douleur imprévus que je vais rencontrer en utilisant un tableau d'objets qui le rend tel que je devrais simplement abandonner planifier et essayer de s'en tenir à un objet identifié par ID?
C'est probablement une recommandation pour des applications plus complexes avec beaucoup de réponses d'API profondément imbriquées. Cependant, dans votre exemple particulier, cela n’a pas tellement d'importance.