Je réécris mon application pour utiliser Flux et j'ai un problème avec la récupération des données des magasins. J'ai beaucoup de composants et ils s'emboîtent beaucoup. Certains sont grands (Article
), certains sont petits et simples (UserAvatar
, UserLink
).
J'ai eu du mal à savoir où, dans la hiérarchie des composants, je devrais lire les données des magasins.
J'ai essayé deux approches extrêmes, dont aucune ne m'a plu:
Chaque composant qui a besoin de certaines données de Store reçoit uniquement l'ID d'entité et récupère l'entité de son propre chef.
Par exemple, Article
est transmis articleId
, UserAvatar
et UserLink
sont transmis userId
.
Cette approche présente plusieurs inconvénients importants (examinés sous l'exemple de code).
var Article = React.createClass({
mixins: [createStoreMixin(ArticleStore)],
propTypes: {
articleId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
article: ArticleStore.get(this.props.articleId);
}
},
render() {
var article = this.state.article,
userId = article.userId;
return (
<div>
<UserLink userId={userId}>
<UserAvatar userId={userId} />
</UserLink>
<h1>{article.title}</h1>
<p>{article.text}</p>
<p>Read more by <UserLink userId={userId} />.</p>
</div>
)
}
});
var UserAvatar = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<img src={user.thumbnailUrl} />
)
}
});
var UserLink = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<Link to='user' params={{ userId: this.props.userId }}>
{this.props.children || user.name}
</Link>
)
}
});
Inconvénients de cette approche:
Quand j'étais fatigué de traquer les bogues, j'ai essayé de mettre toutes les données à récupérer au plus haut niveau. Cependant, cela s'est avéré impossible car pour certaines entités, j'ai plusieurs niveaux d'imbrication.
Par exemple:
Category
contient UserAvatar
des personnes qui contribuent à cette catégorie;Article
peut avoir plusieurs Category
.Par conséquent, si je voulais récupérer toutes les données des magasins au niveau d'un Article
, je devrais:
ArticleStore
;CategoryStore
;UserStore
;Encore plus frustrant, chaque fois que j'ai besoin d'une entité profondément imbriquée, je devrais ajouter du code à chaque niveau d'imbrication pour le transmettre en plus.
Les deux approches semblent défectueuses. Comment résoudre ce problème de la manière la plus élégante?
Mes objectifs:
Les magasins ne devraient pas avoir un nombre insensé d'abonnés. Il est stupide pour chaque UserLink
d'écouter UserStore
si les composants parents le font déjà.
Si le composant parent a récupéré un objet du magasin (par exemple user
), je ne veux pas qu'un composant imbriqué doive le récupérer à nouveau. Je devrais pouvoir le passer via des accessoires.
Je ne devrais pas avoir à récupérer toutes les entités (y compris les relations) au niveau supérieur, car cela compliquerait l'ajout ou la suppression de relations. Je ne veux pas introduire de nouveaux accessoires à tous les niveaux d'imbrication chaque fois qu'une entité imbriquée obtient une nouvelle relation (par exemple, la catégorie obtient un curator
).
L'approche à laquelle je suis arrivé consiste à faire en sorte que chaque composant reçoive ses données (et non ses ID) comme accessoire. Si un composant imbriqué a besoin d'une entité associée, c'est au composant parent de le récupérer.
Dans notre exemple, Article
devrait avoir un article
prop qui est un objet (vraisemblablement récupéré par ArticleList
ou ArticlePage
).
Parce que Article
souhaite également rendre UserLink
et UserAvatar
pour l'auteur de l'article, il s'abonnera à UserStore
et gardera author: UserStore.get(article.authorId)
dans son état . Il rendra ensuite UserLink
et UserAvatar
avec ce this.state.author
. S'ils souhaitent le transmettre davantage, ils le peuvent. Aucun composant enfant ne devra à nouveau récupérer cet utilisateur.
Recommencer:
Cela résout très bien mon problème. Exemple de code réécrit pour utiliser cette approche:
var Article = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
article: PropTypes.object.isRequired
},
getStateFromStores() {
return {
author: UserStore.get(this.props.article.authorId);
}
},
render() {
var article = this.props.article,
author = this.state.author;
return (
<div>
<UserLink user={author}>
<UserAvatar user={author} />
</UserLink>
<h1>{article.title}</h1>
<p>{article.text}</p>
<p>Read more by <UserLink user={author} />.</p>
</div>
)
}
});
var UserAvatar = React.createClass({
propTypes: {
user: PropTypes.object.isRequired
},
render() {
var user = this.props.user;
return (
<img src={user.thumbnailUrl} />
)
}
});
var UserLink = React.createClass({
propTypes: {
user: PropTypes.object.isRequired
},
render() {
var user = this.props.user;
return (
<Link to='user' params={{ userId: this.props.user.id }}>
{this.props.children || user.name}
</Link>
)
}
});
Cela rend les composants les plus internes stupides mais ne nous oblige pas à compliquer l'enfer des composants de haut niveau.
La plupart des gens commencent par écouter les magasins appropriés dans un composant de vue contrôleur près du sommet de la hiérarchie.
Plus tard, quand il semble que beaucoup d'accessoires non pertinents sont transmis à travers la hiérarchie à un composant profondément imbriqué, certaines personnes décideront que c'est une bonne idée pour permettre à un composant plus profond d'écouter les changements dans les magasins. Cela offre une meilleure encapsulation du domaine problématique sur lequel porte cette branche plus profonde de l'arborescence des composants. Il y a de bons arguments à faire pour le faire judicieusement.
Cependant, je préfère toujours écouter en haut et simplement transmettre toutes les données. Je vais même parfois prendre la totalité de l'état du magasin et le transmettre à la hiérarchie en tant qu'objet unique, et je le ferai pour plusieurs magasins. J'aurais donc un accessoire pour l'état de ArticleStore
, et un autre pour l'état de UserStore
, etc. Je trouve qu'éviter les vues de contrôleur profondément imbriquées maintient un point d'entrée unique pour les données et unifie le flux de données. Sinon, j'ai plusieurs sources de données, ce qui peut devenir difficile à déboguer.
La vérification de type est plus difficile avec cette stratégie, mais vous pouvez configurer une "forme", ou un modèle de type, pour le grand objet en tant que prop avec les PropTypes de React. Voir: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91http://facebook.github.io/ react/docs/reusable-components.html # prop-validation
Notez que vous souhaiterez peut-être mettre la logique d'association de données entre les magasins dans les magasins eux-mêmes. Ainsi, votre ArticleStore
pourrait waitFor()
le UserStore
, et inclure les utilisateurs concernés avec chaque Article
enregistrement qu'il fournit via getArticles()
. Faire cela dans vos vues ressemble à pousser la logique dans la couche de vue, ce qui est une pratique que vous devriez éviter autant que possible.
Vous pourriez également être tenté d'utiliser transferPropsTo()
, et beaucoup de gens aiment faire cela, mais je préfère garder tout explicite pour la lisibilité et donc la maintenabilité.
FWIW, je crois comprendre que David Nolen adopte une approche similaire avec son framework Om (qui est quelque peu compatible avec Flux ) avec un seul point d'entrée de données sur le nœud racine - l'équivalent dans Flux serait d'avoir une seule vue contrôleur écoutant tous les magasins. Ceci est rendu efficace en utilisant shouldComponentUpdate()
et des structures de données immuables qui peuvent être comparées par référence, avec ===. Pour les structures de données immuables, consultez David's mori ou Facebook immutable-js . Ma connaissance limitée d'Om vient principalement de The Future of JavaScript MVC Frameworks
Ma solution est beaucoup plus simple. Chaque composant qui a son propre état est autorisé à parler et à écouter les magasins. Ce sont des composants très semblables à des contrôleurs. Les composants imbriqués plus profonds qui ne conservent pas l'état mais qui ne rendent que des éléments ne sont pas autorisés. Ils ne reçoivent que des accessoires pour un rendu pur, très proche de la vue.
De cette façon, tout passe des composants avec état aux composants sans état. Garder le nombre d'états bas.
Dans votre cas, Article serait avec état et donc les discussions avec les magasins et UserLink etc. ne seraient rendues que pour recevoir article.user comme accessoire.
Les problèmes décrits dans vos 2 philosophies sont communs à toute application d'une seule page.
Ils sont abordés brièvement dans cette vidéo: https://www.youtube.com/watch?v=IrgHurBjQbg et Relay ( https://facebook.github.io/relay =) a été développé par Facebook pour surmonter le compromis que vous décrivez.
L'approche du relais est très centrée sur les données. Il s'agit d'une réponse à la question "Comment puis-je obtenir uniquement les données nécessaires pour chaque composant de cette vue dans une seule requête vers le serveur?" Et en même temps, Relay s'assure que vous avez peu de couplage à travers le code lorsqu'un composant est utilisé dans plusieurs vues.
Si le relais n'est pas une option, "Tous les composants d'entité lisent leurs propres données" me semble une meilleure approche pour la situation que vous décrivez. Je pense que l'idée fausse dans Flux est ce qu'est un magasin. Le concept de magasin n'existe pas pour être le lieu de conservation d'un modèle ou d'une collection d'objets. Les magasins sont des emplacements temporaires où votre application place les données avant le rendu de la vue. La vraie raison de leur existence est de résoudre le problème des dépendances entre les données stockées dans différents magasins.
Ce que Flux ne précise pas, c'est comment un magasin est lié au concept de modèles et de collection d'objets (à la dorsale). En ce sens, certaines personnes font en fait d'un magasin de flux un endroit où mettre une collection d'objets d'un type spécifique qui n'est pas à ras pendant tout le temps que l'utilisateur garde le navigateur ouvert mais, si je comprends bien le flux, ce n'est pas ce qu'un magasin est censé être.
La solution est d'avoir une autre couche où vous où les entités nécessaires pour rendre votre vue (et potentiellement plus) sont stockées et mises à jour. Si vous cette couche qui résume les modèles et les collections, ce n'est pas un problème si vous, les sous-composants, devez interroger à nouveau pour obtenir leurs propres données.