web-dev-qa-db-fra.com

À quel niveau d'imbrication les composants devraient-ils lire les entités des magasins dans 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 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:

Tous les composants d'entité lisent leurs propres données

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:

  • Il est frustrant d'avoir des composants 100s potentiellement abonnés aux magasins;
  • C'est difficile de garder une trace de la façon dont les données sont mises à jour et dans quel ordre parce que chaque composant récupère ses données indépendamment;
  • Même si vous avez déjà une entité en état, vous êtes obligé de transmettre son ID aux enfants, qui le récupéreront à nouveau (ou bien casser la cohérence ).

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

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:

  • Un Category contient UserAvatar des personnes qui contribuent à cette catégorie;
  • Un 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:

  • Récupérer l'article de ArticleStore;
  • Récupérez toutes les catégories d'articles de CategoryStore;
  • Récupérez séparément les contributeurs de chaque catégorie à partir de UserStore;
  • Transmettez en quelque sorte 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 du code à chaque niveau d'imbrication pour le transmettre en plus.

Résumer

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

87
Dan Abramov

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:

  • Aucun composant ne reçoit jamais d'ID comme accessoire; tous les composants reçoivent leurs objets respectifs.
  • Si les composants enfants ont besoin d'une entité, il est de la responsabilité du parent de la récupérer et de la passer en tant qu'accessoire.

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.

36
Dan Abramov

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

36
fisherwebdev

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.

1
Rygu

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.

0
Chris Cinelli