À quel niveau de nidification, les composants devraient-ils lire des entités dans les magasins de Flux?

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 nichent beaucoup. Certains d'entre eux sont grands ( Article ), certains sont petits et simples ( UserAvatar , UserLink ).

Je me suis heurté avec la hiérarchie des composants que je devais lire dans les magasins.
J'ai essayé deux approches extrêmes, dont j'ai tout à fait plaisir:

Tous les composants de l'entité lisent leurs propres données

Chaque composant qui a besoin de certaines données de Store reçoit juste ID d'entité et récupère l'entité seule.
Par exemple, l' Article est passé articleId , UserAvatar et UserLink sont passés userId .

Cette approche comporte plusieurs inconvénients significatifs (discuté sous un 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> ) } }); 

Les inconvénients de cette approche:

  • Il est frustrant d'avoir des composants de 100s pouvant s'abonner aux magasins;
  • Il est difficile de suivre la façon dont les données sont mises à jour et dans quel ordre parce que chaque composant récupère ses données de façon indépendante;
  • Même si vous pourriez avoir déjà une entité dans l'état, vous êtes obligé de passer son ID aux enfants, qui le récupérera (ou briser la cohérence).

Toutes les données sont lues une fois au niveau supérieur et transmises aux composants

Quand j'étais fatigué de suivre les bugs, j'ai essayé de mettre toutes les données en train de récupérer au niveau supérieur. Ceci, cependant, s'est avéré impossible parce que pour certaines entités j'ai plusieurs niveaux de nidification.

Par exemple:

  • Une Category contient UserAvatar s de personnes qui contribuent à cette catégorie;
  • Un Article peut avoir plusieurs Category s.

Par conséquent, si je voulais récupérer toutes les données des magasins au niveau d'un Article , je devrais:

  • Récupérer un article de ArticleStore ;
  • Récupérer toutes les catégories d'articles de CategoryStore ;
  • Récupérez séparément les contributeurs de chaque catégorie de UserStore ;
  • D'une manière ou d'une autre, transmettez toutes ces données aux composants.

Encore plus frustrant, chaque fois que j'ai besoin d'une entité profondément imbriquée, je devrais ajouter un code à chaque niveau d'imbrication pour le décoller.

Résumant

Les deux approches semblent fausses. Comment résoudre ce problème de façon aussi élégante?

Mes objectifs:

  • Les magasins ne devraient pas avoir un nombre insensé d'abonnés. C'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, l' user ), je ne veux pas que les composants imbriqués devaient le récupérer. Je devrais pouvoir passer par des accessoires.

  • Je ne devrais pas chercher toutes les entités (y compris les relations) au niveau supérieur, car cela compliquerait l'ajout ou l'enlèvement de relations. Je ne souhaite pas introduire de nouveaux accessoires à tous les niveaux de nidification chaque fois qu'une entité imbriquée obtient une nouvelle relation (par exemple, la catégorie obtient un curator ).

La plupart des gens commencent en écoutant les magasins concernés dans un composant de contrôleur-vue près du haut de la hiérarchie.

Plus tard, quand il semble que beaucoup d'accessoires non pertinents sont transmis par la hiérarchie à un composant profondément imbriqué, certaines personnes décideront que c'est une bonne idée de laisser un composant plus profond écouter les changements dans les magasins. Cela offre une meilleure encapsulation du domaine du problème sur lequel cette branche plus profonde de l'arbre des composants est à propos. Il y a de bons arguments à faire pour le faire judicieusement.

Cependant, je préfère toujours écouter en haut et simplement passer toutes les données. Je prendrais parfois l'état entier du magasin et je le transmettrai par la hiérarchie comme un objet unique, et je le ferai pour plusieurs magasins. Donc, je disposerais d'un support pour l'état de l' UserStore et d'un autre pour l' UserStore du UserStore , etc. Je trouve que l'évitement des vues de contrôle 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-comme-support avec PropTypes de React. Voir: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -validation

Notez que vous voudrez peut-être mettre la logique d'association de données entre les magasins dans les magasins eux-mêmes. Ainsi, votre waitFor() pourrait waitFor() le UserStore et inclure les utilisateurs concernés avec chaque enregistrement d' Article 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 facilité d'entretien.

FWIW, je crois comprendre que David Nolen adopte une approche similaire avec son framework Om (ce qui est un peu compatible avec le flux ) avec un seul point d'entrée de données sur le noeud racine – l'équivalent dans Flux devrait uniquement avoir une vue contrôleur Écouter tous les magasins. Cela est rendu efficace en utilisant shouldComponentUpdate() et les structures de données immuables qui peuvent être comparées par référence, avec ===. Pour les structures de données immuables, vérifiez David's Mori ou Facebook- immutable-js . Ma connaissance limitée d'Om provient principalement de l'avenir des framework MVC JavaScript

L'approche à laquelle je suis arrivé consiste à faire en sorte que chaque composant reçoive ses données (pas des identifiants) en tant que support. Si un composant imbriqué nécessite une entité apparentée, il appartient au composant parent de le récupérer.

Dans notre exemple, l' Article devrait avoir un article prop qui est un objet (vraisemblablement récupéré par ArticleList ou ArticlePage ).

Parce que l' Article veut également rendre UserLink et UserAvatar pour l'auteur de l'article, il s'abonnera à UserStore et gardera l' author: UserStore.get(article.authorId) dans son état. Il va ensuite rendre UserLink et UserAvatar avec this this.state.author . S'ils souhaitent passer plus loin, ils peuvent. Aucun composant enfant ne devra récupérer cet utilisateur à nouveau.

Recommencer:

  • Aucun composant jamais reçu l'ID comme support; Tous les composants reçoivent leurs objets respectifs.
  • Si les composants enfants ont besoin d'une entité, il incombe aux parents de le récupérer et de passer en tant que support technique.

Cela résout mon problème très bien. 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 intimes stupides mais ne nous oblige pas à compliquer l'enfance des composants de haut niveau.

Ma solution est beaucoup plus simple. Chaque composant possédant 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 maintiennent pas l'état mais ne font que rendre des objets ne sont pas autorisés. Ils ne reçoivent que des accessoires pour un rendu pur, très similaire à la vue.

De cette façon, tout va de composants éminents à des composants sans état. Garder les états-majors très bas.

Dans votre cas, l'article serait confirmé et par conséquent parle aux magasins et à UserLink, etc. ne le rendraient que pour qu'il recevât article.user comme support.

Les problèmes décrits dans vos 2 philosophies sont communs à une application de page unique.

Ils sont discuté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 de Relay est très centrée sur les données. C'est une réponse à la question "Comment puis-je obtenir les données nécessaires pour chaque composant dans cette vue dans une requête du serveur?" Et en même temps, Relay s'assure que vous avez peu de couplage sur le code lorsqu'un composant utilisé dans plusieurs vues.

Si Relay n'est pas une option , «Tous les composants de l'entité lisent leurs propres données» me paraissent mieux 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 l'endroit où un modèle ou une collection d'objets sont conservés. Les magasins sont des endroits temporaires où votre application met les données avant que la vue ne soit rendue. La vraie raison pour laquelle ils existent est de résoudre le problème des dépendances à travers les données qui se situent dans différents magasins.

Ce que Flux ne spécifie pas, c'est la façon dont un magasin se rapporte au concept de modèles et à la collecte d'objets (a la Backbone). En ce sens, certaines personnes font en fait un magasin de flux où placer une collection d'objets d'un type spécifique qui n'est pas flush pendant tout le temps que l'utilisateur maintient le navigateur ouvert mais, si je comprends le flux, ce n'est pas ce qu'un magasin est censé être.

La solution consiste à disposer d'une autre couche où les entités nécessaires pour rendre votre vue (et potentiellement plus) sont stockées et mises à jour. Si vous avez cette couche qui abstrait des modèles et des collections, ce n'est pas un problème si vous devez recommencer les sous-composants à obtenir leurs propres données.