Auf welcher Verschachtelungsebene sollten Komponenten Entitäten aus Stores in Flux lesen?

89

Ich schreibe meine App neu, um Flux zu verwenden, und habe ein Problem beim Abrufen von Daten aus Stores. Ich habe viele Komponenten und sie nisten viel. Einige von ihnen sind groß ( Article), andere klein und einfach ( UserAvatar, UserLink).

Ich habe Probleme damit, wo in der Komponentenhierarchie ich Daten aus Stores lesen sollte.
Ich habe zwei extreme Ansätze ausprobiert, von denen mir keiner gefallen hat:

Alle Entitätskomponenten lesen ihre eigenen Daten

Jede Komponente, die einige Daten aus dem Store benötigt, empfängt nur die Entitäts-ID und ruft die Entität selbst ab.
Zum Beispiel Articleist vergangen articleId, UserAvatarund UserLinkweitergegeben werden userId.

Dieser Ansatz hat mehrere wesentliche Nachteile (siehe Codebeispiel).

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>
    )
  }
});

Nachteile dieses Ansatzes:

  • Es ist frustrierend, dass 100s-Komponenten möglicherweise Stores abonnieren.
  • Es ist schwierig zu verfolgen, wie Daten in welcher Reihenfolge aktualisiert werden, da jede Komponente ihre Daten unabhängig abruft.
  • Auch wenn sich möglicherweise bereits eine Entität im Status befindet, müssen Sie ihre ID an untergeordnete Elemente weitergeben, die sie erneut abrufen (oder die Konsistenz beeinträchtigen).

Alle Daten werden einmal auf der obersten Ebene gelesen und an die Komponenten weitergegeben

Als ich es satt hatte, Fehler aufzuspüren, habe ich versucht, alle Daten, die abgerufen wurden, auf die oberste Ebene zu bringen. Dies erwies sich jedoch als unmöglich, da ich für einige Entitäten mehrere Verschachtelungsebenen habe.

Beispielsweise:

  • A Categoryenthält UserAvatars von Personen, die zu dieser Kategorie beitragen;
  • Ein Articlekann mehrere Categorys haben.

Wenn ich also alle Daten aus Stores auf der Ebene von abrufen Articlemöchte, muss ich:

  • Artikel abrufen von ArticleStore;
  • Rufen Sie alle Artikelkategorien von ab CategoryStore.
  • Rufen Sie die Mitwirkenden jeder Kategorie separat ab UserStore.
  • Übergeben Sie all diese Daten irgendwie an Komponenten.

Noch frustrierender ist, dass ich, wenn ich eine tief verschachtelte Entität benötige, jeder Verschachtelungsebene Code hinzufügen muss, um sie zusätzlich weiterzugeben.

Zusammenfassen

Beide Ansätze scheinen fehlerhaft zu sein. Wie löse ich dieses Problem am elegantesten?

Meine Ziele:

  • Geschäfte sollten nicht wahnsinnig viele Abonnenten haben. Es ist dumm UserLink, UserStorewenn jeder zuhört, wenn übergeordnete Komponenten dies bereits tun.

  • Wenn die übergeordnete Komponente ein Objekt aus dem Speicher abgerufen hat (z. B. user), möchte ich nicht, dass verschachtelte Komponenten es erneut abrufen müssen. Ich sollte es über Requisiten weitergeben können.

  • Ich sollte nicht alle Entitäten (einschließlich Beziehungen) auf der obersten Ebene abrufen müssen, da dies das Hinzufügen oder Entfernen von Beziehungen erschweren würde. Ich möchte nicht jedes Mal neue Requisiten auf allen Verschachtelungsebenen einführen, wenn eine verschachtelte Entität eine neue Beziehung erhält (z. B. Kategorie erhält eine curator).

Dan Abramov
quelle
1
Vielen Dank für die Veröffentlichung. Ich habe gerade eine sehr ähnliche Frage hier gestellt: groups.google.com/forum/… Alles, was ich gesehen habe, hat sich eher mit flachen Datenstrukturen als mit zugehörigen Daten befasst. Ich bin überrascht, keine Best Practices dafür zu sehen.
Uberllama

Antworten:

37

Die meisten Benutzer beginnen mit dem Abhören der relevanten Speicher in einer Controller-Ansichtskomponente am oberen Rand der Hierarchie.

Später, wenn es so aussieht, als würden viele irrelevante Requisiten durch die Hierarchie an eine tief verschachtelte Komponente weitergegeben, einige Leute entscheiden, dass es eine gute Idee ist, eine tiefere Komponente auf Änderungen in den Läden warten zu lassen. Dies bietet eine bessere Kapselung der Problemdomäne, um die es in diesem tieferen Zweig des Komponentenbaums geht. Dafür gibt es gute Argumente.

Ich höre jedoch lieber immer oben zu und gebe einfach alle Daten weiter. Manchmal nehme ich sogar den gesamten Status des Geschäfts und übergebe ihn als einzelnes Objekt durch die Hierarchie. Dies mache ich für mehrere Geschäfte. Also hätte ich eine Requisite für den ArticleStoreStaat und eine andere für dieUserStore Status usw. Ich finde, dass das Vermeiden tief verschachtelter Controller-Ansichten einen singulären Einstiegspunkt für die Daten beibehält und den Datenfluss vereinheitlicht. Andernfalls habe ich mehrere Datenquellen, und das Debuggen kann schwierig werden.

Die Typprüfung ist mit dieser Strategie schwieriger, aber Sie können mit den PropTypes von React eine "Form" oder eine Typvorlage für das große Objekt als Requisite einrichten. Siehe: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -Validierung

Beachten Sie, dass Sie möglicherweise die Logik der Zuordnung von Daten zwischen Filialen in die Filialen selbst einfügen möchten. Sie ArticleStorekönnen also waitFor()die UserStorerelevanten Benutzer in jeden ArticleDatensatz einbeziehen, den sie bereitstellen getArticles(). Wenn Sie dies in Ihren Ansichten tun, wird die Logik in die Ansichtsebene verschoben. Dies sollten Sie nach Möglichkeit vermeiden.

Sie könnten auch versucht sein, dies zu verwenden transferPropsTo(), und viele Leute tun dies gerne, aber ich bevorzuge es, alles explizit zu halten, um die Lesbarkeit und damit die Wartbarkeit zu gewährleisten.

FWIW, ich verstehe, dass David Nolen mit seinem Om-Framework (das etwas Flux-kompatibel ist ) einen ähnlichen Ansatz mit einem einzigen Dateneintrittspunkt auf dem Stammknoten verfolgt - das Äquivalent in Flux wäre, nur eine Controller-Ansicht zu haben alle Geschäfte hören. Dies wird effizient gemacht, indem shouldComponentUpdate()unveränderliche Datenstrukturen verwendet werden, die als Referenz mit === verglichen werden können. Informationen zu unveränderlichen Datenstrukturen finden Sie in Davids Mori oder in Facebooks unveränderlichen Daten . Mein begrenztes Wissen über Om stammt hauptsächlich aus der Zukunft von JavaScript MVC Frameworks

Fisherwebdev
quelle
4
Vielen Dank für eine ausführliche Antwort! Ich habe darüber nachgedacht, beide Schnappschüsse des Geschäftszustands weiterzugeben. Dies kann jedoch zu einem Perf-Problem führen, da bei jedem Update von UserStore alle Artikel auf dem Bildschirm neu gerendert werden und PureRenderMixin nicht erkennen kann, welche Artikel aktualisiert werden müssen und welche nicht. Was Ihren zweiten Vorschlag betrifft, habe ich auch darüber nachgedacht, aber ich kann nicht sehen, wie ich die Gleichheit der Artikelreferenzen beibehalten kann, wenn der Benutzer des Artikels bei jedem Aufruf von getArticles () "injiziert" wird. Dies würde mir wiederum möglicherweise Perfektionsprobleme bereiten.
Dan Abramov
1
Ich verstehe Ihren Standpunkt, aber es gibt Lösungen. Zum einen können Sie shouldComponentUpdate () einchecken, wenn sich ein Array von Benutzer-IDs für einen Artikel geändert hat. Dies bedeutet im Grunde nur, dass Sie die Funktionalität und das Verhalten, die Sie in diesem speziellen Fall in PureRenderMixin wirklich benötigen, von Hand rollen.
Fisherwebdev
3
Richtig, und trotzdem müsste ich es nur tun, wenn ich sicher bin, dass es Perf-Probleme gibt , die bei Geschäften, die mehrmals pro Minute aktualisiert werden, nicht der Fall sein sollten. Danke nochmal!
Dan Abramov
8
Je nachdem, welche Art von Anwendung Sie erstellen, wird es schaden, einen einzigen Einstiegspunkt beizubehalten, sobald mehr "Widgets" auf dem Bildschirm angezeigt werden, die nicht direkt zum selben Stamm gehören. Wenn Sie dies mit Nachdruck versuchen, hat dieser Stammknoten viel zu viel Verantwortung. KISS und SOLID, obwohl es clientseitig ist.
Rygu
37

Der Ansatz, zu dem ich gekommen bin, besteht darin, dass jede Komponente ihre Daten (nicht IDs) als Requisite erhält. Wenn eine verschachtelte Komponente eine verwandte Entität benötigt, muss sie von der übergeordneten Komponente abgerufen werden.

In unserem Beispiel Articlesollte eine articleRequisite ein Objekt sein (vermutlich von ArticleListoder abgerufen ArticlePage).

Denn Articleauch machen will , UserLinkund UserAvatarfür Artikel des Autors, wird es abonniertUserStore und halten author: UserStore.get(article.authorId)in seinem Zustand. Es wird dann gerendert UserLinkund UserAvatardamit this.state.author. Wenn sie es weitergeben wollen, können sie es. Keine untergeordneten Komponenten müssen diesen Benutzer erneut abrufen.

Wiederholen:

  • Keine Komponente erhält jemals einen Ausweis als Requisite. Alle Komponenten erhalten ihre jeweiligen Objekte.
  • Wenn untergeordnete Komponenten eine Entität benötigen, liegt es in der Verantwortung der Eltern, diese abzurufen und als Requisite zu übergeben.

Dies löst mein Problem ganz gut. Codebeispiel neu geschrieben, um diesen Ansatz zu verwenden:

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>
    )
  }
});

Dies hält die innersten Komponenten dumm, zwingt uns aber nicht dazu, die Komponenten der obersten Ebene zum Teufel zu machen.

Dan Abramov
quelle
Nur um dieser Antwort eine Perspektive hinzuzufügen. Konzeptionell können Sie einen Begriff des Eigentums entwickeln. Wenn eine Datenerfassung in der obersten Ansicht abgeschlossen ist, wem die Daten tatsächlich gehören (und daher ihren Speicher abhören). Einige Beispiele: 1. AuthData sollte Eigentum von App Component sein. Jede Änderung von Auth sollte von App Component als Requisit an alle untergeordneten Elemente weitergegeben werden. 2. ArticleData sollte im Besitz von ArticlePage oder Article List sein, wie in der Antwort vorgeschlagen. Jetzt kann jedes untergeordnete Element von ArticleList AuthData und / oder ArticleData von ArticleList empfangen, unabhängig davon, welche Komponente diese Daten tatsächlich abgerufen hat.
Saumitra R. Bhave
2

Meine Lösung ist viel einfacher. Jede Komponente, die ihren eigenen Status hat, darf mit Geschäften sprechen und diese abhören. Dies sind sehr Controller-ähnliche Komponenten. Tiefere verschachtelte Komponenten, die den Status nicht beibehalten, sondern nur Inhalte rendern, sind nicht zulässig. Sie erhalten nur Requisiten für reines Rendering, sehr anschaulich.

Auf diese Weise fließt alles von zustandsbehafteten Komponenten in zustandslose Komponenten. Die Anzahl der Statefuls niedrig halten.

In Ihrem Fall wäre Article zustandsbehaftet und daher würden Gespräche mit den Stores und UserLink usw. nur gerendert, sodass article.user als Requisite empfangen würde.

Rygu
quelle
1
Ich nehme an, dies würde funktionieren, wenn der Artikel eine Benutzerobjekteigenschaft hätte, in meinem Fall jedoch nur eine Benutzer-ID (da der tatsächliche Benutzer im UserStore gespeichert ist). Oder vermisse ich deinen Standpunkt? Würden Sie ein Beispiel teilen?
Dan Abramov
Initiieren Sie einen Abruf für den Benutzer im Artikel. Oder ändern Sie Ihr API-Backend, um die Benutzer-ID "erweiterbar" zu machen, damit Sie den Benutzer sofort erhalten. In beiden Fällen bleiben die tieferen verschachtelten Komponenten in einfachen Ansichten und nur auf Requisiten angewiesen.
Rygu
2
Ich kann es mir nicht leisten, den Abruf im Artikel zu initiieren, da er zusammen gerendert werden muss. Erweiterbare APIs hatten wir früher , aber es ist sehr problematisch, sie aus Stores zu analysieren. Ich habe sogar eine Bibliothek geschrieben , um die Antworten aus diesem Grund zu reduzieren.
Dan Abramov
0

Die in Ihren beiden Philosophien beschriebenen Probleme treten bei jeder einzelnen Seitenanwendung auf.

Sie werden in diesem Video kurz erläutert: https://www.youtube.com/watch?v=IrgHurBjQbg und Relay ( https://facebook.github.io/relay ) wurden von Facebook entwickelt, um den von Ihnen beschriebenen Kompromiss zu überwinden.

Der Ansatz von Relay ist sehr datenorientiert. Es ist eine Antwort auf die Frage "Wie erhalte ich nur die erforderlichen Daten für jede Komponente in dieser Ansicht in einer Abfrage an den Server?" Gleichzeitig stellt Relay sicher, dass der Code nur wenig gekoppelt ist, wenn eine Komponente in mehreren Ansichten verwendet wird.

Wenn Relay keine Option ist , scheint mir "Alle Entitätskomponenten lesen ihre eigenen Daten" für die von Ihnen beschriebene Situation ein besserer Ansatz zu sein. Ich denke, das Missverständnis in Flux ist, was ein Geschäft ist. Das Konzept des Geschäfts existiert nicht als Ort, an dem ein Modell oder eine Sammlung von Objekten aufbewahrt wird. Speicher sind temporäre Orte, an denen Ihre Anwendung die Daten ablegt, bevor die Ansicht gerendert wird. Der wahre Grund, warum sie existieren, besteht darin, das Problem der Abhängigkeiten zwischen den Daten zu lösen, die in verschiedenen Speichern gespeichert sind.

Was Flux nicht spezifiziert, ist, wie sich ein Geschäft auf das Konzept von Modellen und die Sammlung von Objekten bezieht (a la Backbone). In diesem Sinne machen einige Leute einen Flussmittelspeicher tatsächlich zu einem Ort, an dem eine Sammlung von Objekten eines bestimmten Typs abgelegt werden kann, der nicht für die gesamte Zeit bündig ist, in der der Benutzer den Browser geöffnet hält, aber nach meinem Verständnis von Flussmitteln ist dies kein Speicher sollte sein.

Die Lösung besteht darin, eine weitere Ebene zu haben, auf der Sie die zum Rendern Ihrer Ansicht erforderlichen Entitäten (und möglicherweise weitere) speichern und auf dem neuesten Stand halten. Wenn Sie diese Ebene, die Modelle und Sammlungen abstrahiert, ist es kein Problem, wenn Sie die Unterkomponenten erneut abfragen müssen, um ihre eigenen Daten zu erhalten.

Chris Cinelli
quelle
1
Soweit ich weiß, können wir mit Dans Ansatz, verschiedene Arten von Objekten zu behalten und durch IDs zu referenzieren, den Cache dieser Objekte behalten und sie nur an einem Ort aktualisieren. Wie kann dies erreicht werden, wenn Sie beispielsweise mehrere Artikel haben, die auf denselben Benutzer verweisen, und dann der Name des Benutzers aktualisiert wird? Die einzige Möglichkeit besteht darin, alle Artikel mit neuen Benutzerdaten zu aktualisieren. Dies ist genau das gleiche Problem, das Sie erhalten, wenn Sie für Ihr Backend zwischen Dokument und relationaler Datenbank wählen. Was denkst du darüber? Danke dir.
Alex Ponomarev