À quel niveau d'imbrication les composants doivent-ils lire les entités des Stores dans Flux?

89

Je réécris mon application pour utiliser Flux et j'ai un problème avec la récupération de données dans les magasins. J'ai beaucoup de composants et ils s'emboîtent beaucoup. Certains d'entre eux sont grands ( Article), certains sont petits et simples ( UserAvatar, UserLink).

J'ai eu du mal à savoir où dans la hiérarchie des composants je devais lire les données des magasins.
J'ai essayé deux approches extrêmes, dont aucune ne m'a beaucoup plu:

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

Chaque composant qui a besoin de certaines données du Store reçoit uniquement l'ID d'entité et récupère l'entité par lui-même.
Par exemple, Articleest passé articleId, UserAvataret UserLinksont passés userId.

Cette approche présente plusieurs inconvénients importants (décrits dans 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 centaines de composants potentiellement abonnés à des magasins;
  • Il est difficile de savoir comment les données sont mises à jour et dans quel ordre, car chaque composant récupère ses données indépendamment;
  • Même si vous avez peut-être déjà une entité dans l'état, vous êtes obligé de transmettre son ID aux enfants, qui la récupéreront à nouveau (ou bien rompront 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érées au plus haut niveau. Cela s'est toutefois avéré impossible car pour certaines entités, j'ai plusieurs niveaux d'imbrication.

Par exemple:

  • A Categorycontient UserAvatardes personnes qui contribuent à cette catégorie;
  • Un Articlepeut avoir plusieurs Categorys.

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

  • Récupérer l'article de ArticleStore;
  • Récupérez toutes les catégories d'articles à partir de CategoryStore;
  • Récupérez séparément les contributeurs de chaque catégorie 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 imparfaites. 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. C'est stupide pour chacun UserLinkd'écouter UserStoresi 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 transmettre 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).

Dan Abramov
la source
1
Merci d'avoir publié ceci. Je viens de poster une question très similaire ici: groups.google.com/forum/ ... Tout ce que j'ai vu concerne des structures de données plates plutôt que des données associées. Je suis surpris de ne voir aucune meilleure pratique à ce sujet.
uberllama

Réponses:

37

La plupart des gens commencent par écouter les magasins pertinents dans un composant de vue contrôleur près du haut de la hiérarchie.

Plus tard, quand il semble que de nombreux 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 de laisser un composant plus profond écouter les changements dans les magasins. Cela offre une meilleure encapsulation du domaine du problème sur lequel porte cette branche plus profonde de l'arborescence des composants. Il y a de bons arguments à faire valoir pour faire cela judicieusement.

Cependant, je préfère toujours écouter en haut et simplement transmettre toutes les données. Je prendrai parfois même l'état entier du magasin et le transmettrai à travers la hiérarchie en tant qu'objet unique, et je le ferai pour plusieurs magasins. J'aurais donc un accessoire pour l' ArticleStoreétat 's, et un autre pour l' UserStoreétat' s, etc. Je trouve qu'éviter les vues de contrôleur profondément imbriquées maintient un point d'entrée singulier pour les données, et unifie le flux de données. Sinon, j'ai plusieurs sources de données, et cela 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-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -validation

Notez que vous souhaiterez peut-être placer la logique d'association de données entre les magasins dans les magasins eux-mêmes. Donc, vous ArticleStorepouvez waitFor()le faire UserStoreet inclure les utilisateurs concernés avec chaque Articleenregistrement qu'il fournit getArticles(). Faire cela dans vos vues ressemble à pousser la logique dans la couche de vue, ce que vous devez éviter autant que possible.

Vous pourriez également être tenté d'utiliser transferPropsTo(), et beaucoup de gens aiment faire cela, mais je préfère tout garder 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 de n'avoir qu'une seule vue de contrôleur écoute de tous les magasins. Ceci est rendu efficace en utilisant shouldComponentUpdate()des structures de données immuables qui peuvent être comparées par référence, avec ===. Pour les structures de données immuables, consultez le mori de David ou immutable-js de Facebook . Ma connaissance limitée d'Om vient principalement de The Future of JavaScript MVC Frameworks

fisherwebdev
la source
4
Merci pour une réponse détaillée! J'ai envisagé de transmettre les deux instantanés entiers de l'état des magasins. Cependant, cela pourrait me poser un problème de perf car chaque mise à jour d'UserStore entraînera le re-rendu de tous les articles à l'écran, et PureRenderMixin ne pourra pas dire quels articles doivent être mis à jour et lesquels ne le sont pas. Quant à votre deuxième suggestion, j'y ai pensé aussi, mais je ne vois pas comment je pourrais garder l'égalité de référence d'article si l'utilisateur de l'article est "injecté" à chaque appel de getArticles (). Cela me poserait à nouveau des problèmes de performances.
Dan Abramov
1
Je comprends votre point de vue, mais il existe des solutions. D'une part, vous pouvez vérifier shouldComponentUpdate () si un tableau d'ID utilisateur pour un article a changé, ce qui consiste simplement à faire défiler à la main les fonctionnalités et le comportement que vous voulez vraiment dans PureRenderMixin dans ce cas spécifique.
fisherwebdev
3
Bon, et de toute façon je n'aurais besoin de le faire que lorsque je suis certain qu'il y a des problèmes de perf, ce qui ne devrait pas être le cas pour les magasins qui se mettent à jour plusieurs fois par minute max. Merci encore!
Dan Abramov
8
Selon le type d'application que vous construisez, conserver un seul point d'entrée vous fera mal dès que vous commencerez à avoir plus de "widgets" à l'écran qui n'appartiennent pas directement à la même racine. Si vous essayez avec force de le faire de cette façon, ce nœud racine aura beaucoup trop de responsabilités. KISS et SOLID, même si c'est côté client.
Rygu
37

L'approche à laquelle je suis arrivé consiste à faire en sorte que chaque composant reçoive ses données (et non ses identifiants) 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, Articledevrait avoir un articleaccessoire qui est un objet (probablement récupéré par ArticleListou ArticlePage).

Parce que Articleveut aussi rendre UserLinket UserAvatarpour l'auteur de l'article, il s'abonnera UserStoreet restera author: UserStore.get(article.authorId)dans son état. Il sera ensuite rendu UserLinket UserAvataravec cela this.state.author. S'ils souhaitent le transmettre davantage, ils le peuvent. Aucun composant enfant n'aura besoin de récupérer à nouveau cet utilisateur.

Recommencer:

  • Aucun composant ne reçoit jamais d'identifiant 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 comme accessoire.

Cela résout mon problème assez 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 force pas à compliquer l'enfer des composants de haut niveau.

Dan Abramov
la source
Juste pour ajouter une perspective en plus de cette réponse. Conceptuellement, vous pouvez développer une notion de propriété, Étant donné une collecte de données, finaliser en haut la vue la plus propriétaire des données (d'où l'écoute de son magasin). Quelques exemples: 1. AuthData doit appartenir à App Component, tout changement dans Auth doit être propagé en tant que props à tous les enfants par App Component. 2. ArticleData doit appartenir à ArticlePage ou Article List comme suggéré en réponse. maintenant, tout enfant de ArticleList peut recevoir AuthData et / ou ArticleData depuis ArticleList quel que soit le composant qui a réellement récupéré ces données.
Saumitra R. Bhave
2

Ma solution est bien plus simple. Chaque composant qui a son propre état est autorisé à parler et à écouter les magasins. Ce sont des composants de type contrôleur. Les composants imbriqués plus profonds qui ne conservent pas l'état mais qui rendent uniquement des éléments ne sont pas autorisés. Ils ne reçoivent que des accessoires pour un rendu pur, très semblable à une 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 par conséquent parle aux magasins et UserLink etc. ne serait rendu que pour qu'il reçoive article.user comme accessoire.

Rygu
la source
1
Je suppose que cela fonctionnerait si l'article avait une propriété d'objet utilisateur, mais dans mon cas, il n'a que userId (car l'utilisateur réel est stocké dans UserStore). Ou est-ce que je rate votre point? Souhaitez-vous partager un exemple?
Dan Abramov
Lancez une récupération pour l'utilisateur dans l'article. Ou modifiez votre backend API pour rendre l'ID utilisateur "extensible" afin d'obtenir l'utilisateur à la fois. Dans tous les cas, gardez les composants imbriqués plus profonds des vues simples et ne reposent que sur des accessoires.
Rygu
2
Je ne peux pas me permettre de lancer la récupération dans l'article car il doit être rendu ensemble. En ce qui concerne les API extensibles, nous les avions auparavant , mais elles sont très problématiques à analyser à partir des magasins. J'ai même écrit une bibliothèque pour aplatir les réponses pour cette raison même.
Dan Abramov
0

Les problèmes décrits dans vos 2 philosophies sont communs à toute application monopage.

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

Si Relay 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 où un modèle ou une collection d'objets sont conservés. Les magasins sont des emplacements temporaires où votre application place les données avant le rendu de la vue. La vraie raison pour laquelle ils existent est de résoudre le problème des dépendances entre les données qui vont dans différents magasins.

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

La solution est d'avoir une autre couche dans laquelle les entités nécessaires au rendu de votre vue (et potentiellement plus) sont stockées et mises à jour. Si vous utilisez cette couche qui abstrait des modèles et des collections, ce n'est pas un problème si vous les sous-composants doivent à nouveau interroger pour obtenir leurs propres données.

Chris Cinelli
la source
1
Pour autant que je comprends l'approche de Dan, conserver différents types d'objets et les référencer via des identifiants nous permet de garder le cache de ces objets et de les mettre à jour uniquement à un seul endroit. Comment cela peut-il être réalisé si, par exemple, vous avez plusieurs articles qui font référence au même utilisateur, puis le nom de l'utilisateur est mis à jour? Le seul moyen est de mettre à jour tous les articles avec de nouvelles données utilisateur. C'est exactement le même problème que vous rencontrez lorsque vous choisissez entre le document et la base de données relationnelle pour votre backend. Que pensez-vous de cela? Je vous remercie.
Alex Ponomarev