组件应在哪个嵌套级别从 Flux 中的存储中读取实体

At what nesting level should components read entities from Stores in Flux?

本文关键字:Flux 存储 实体 读取 嵌套 组件      更新时间:2023-09-26

我正在重写我的应用程序以使用 Flux,并且在从商店检索数据时遇到问题。我有很多组件,它们嵌套很多。有些是大的(Article(,有些是小而简单的(UserAvatarUserLink(。

我一直在纠结于在组件层次结构中应该从 Stores.
读取数据的问题。我尝试了两种极端的方法,这两种方法我都不太喜欢:

所有实体组件都读取自己的数据

每个需要从应用商店获取一些数据的组件只接收实体 ID 并自行检索实体.
例如,Article articleId传递,UserAvatarUserLink传递userId

此方法有几个明显的缺点(在代码示例中讨论(。

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

这种方法的缺点:

  • 让 100 多个组件可能订阅商店是令人沮丧的;
  • 很难跟踪数据的更新方式和顺序,因为每个组件都独立检索其数据;
  • 即使你可能已经有一个处于状态的实体,你也会被迫将其 ID 传递给子项,子项将再次检索它(否则会破坏一致性(。

所有数据在顶层读取一次并传递给组件

当我厌倦了跟踪错误时,我试图将所有检索数据放在顶层。然而,这被证明是不可能的,因为对于某些实体,我有几个级别的嵌套。

例如:

  • Category包含UserAvatar为该类别做出贡献的人员;
  • 一个Article可能有几个Category

因此,如果我想在Article级别从存储中检索所有数据,我需要:

  • ArticleStore检索文章;
  • CategoryStore中检索所有文章的类别;
  • 分别从UserStore中检索每个类别的贡献者;
  • 以某种方式将所有数据传递给组件。

更令人沮丧的是,每当我需要深度嵌套实体时,我都需要向每个嵌套级别添加代码以额外传递它。

总结

这两种方法似乎都有缺陷。如何最优雅地解决这个问题?

我的目标:

  • 商店不应该有疯狂的订阅者数量。如果父组件已经这样做,那么每个UserLink都听UserStore是愚蠢的。

  • 如果父组件已从存储中检索到某些对象(例如 user (,我不希望任何嵌套组件必须再次获取它。我应该能够通过道具传递它。

  • 我不必在顶层获取所有实体(包括关系(,因为这会使添加或删除关系变得复杂。我不想在每次嵌套实体获得新关系时在所有嵌套级别引入新道具(例如,类别获得curator(。

大多数人从在层次结构顶部附近的控制器视图组件中侦听相关存储开始。

后来,当似乎有很多不相关的道具通过层次结构传递到某个深度嵌套的组件时,有些人会决定让更深层次的组件监听商店中的更改是个好主意。 这提供了对组件树的这个更深分支所关注的问题域的更好封装。 明智地这样做有很好的论据。

但是,我更喜欢始终在顶部收听并简单地传递所有数据。 有时,我甚至会获取存储的整个状态,并将其作为单个对象通过层次结构向下传递,并且我将为多个存储执行此操作。 所以我会有一个用于ArticleStore状态的道具,另一个用于UserStore的状态,等等。 我发现避免深度嵌套的控制器视图可以保持数据的单一入口点,并统一数据流。 否则,我有多个数据源,这可能变得难以调试。

使用这种策略进行类型检查更加困难,但是您可以使用 React 的 PropType 为大型对象即道具设置一个"形状"或类型模板。 看:https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91http://facebook.github.io/react/docs/reusable-components.html#prop-validation

请注意,您可能希望将存储之间关联数据的逻辑放在存储本身中。 因此,您的ArticleStore可能会waitFor() UserStore,并将相关用户包含在通过getArticles()提供的每个Article记录中。 在视图中执行此操作听起来像是将逻辑推送到视图层,这是您应该尽可能避免的做法。

您可能还想使用 transferPropsTo() ,许多人喜欢这样做,但我更喜欢将所有内容都明确用于可读性和可维护性。

FWIW,我的理解是 David Nolen 对他的 Om 框架(有点与 Flux 兼容(采取了类似的方法,在根节点上有一个数据入口点——Flux 中的等效物是只有一个控制器视图侦听所有存储。 通过使用可以通过引用与 === 进行比较的shouldComponentUpdate()和不可变的数据结构,可以提高效率。 对于不可变的数据结构,请查看David的mori或Facebook的不可变-js。 我对 Om 的有限了解主要来自 JavaScript MVC 框架的未来

我得出的方法是让每个组件接收其数据(而不是 ID(作为道具。如果某些嵌套组件需要相关实体,则由父组件来检索它。

在我们的示例中,Article应该有一个article prop,它是一个对象(大概由ArticleListArticlePage检索(。

由于Article还希望为文章作者呈现UserLinkUserAvatar,因此它将订阅UserStore并使author: UserStore.get(article.authorId)保持其状态。然后,它将使用此this.state.author呈现UserLinkUserAvatar。如果他们希望进一步传承下去,他们可以。没有子组件需要再次检索此用户。

重申:

  • 没有组件接收 ID 作为道具;所有组件都接收各自的对象。
  • 如果子组件需要实体,则父组件负责检索它并作为 prop 传递。

这很好地解决了我的问题。重写代码示例以使用此方法:

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

这使最内部的组件保持愚蠢,但不会迫使我们将顶级组件复杂化。

我的解决方案要简单得多。允许每个具有自己状态的组件与商店交谈和侦听。这些是非常类似于控制器的组件。不允许使用不维护状态而仅呈现内容的更深层次的嵌套组件。他们只接收纯渲染的道具,非常像视图。

这样,一切都从有状态组件流向无状态组件。保持低状态计数。

在您的情况下,文章将是有状态的,因此与商店和用户链接等的对话只会呈现,以便接收 article.user 作为道具。

您的 2 个哲学中描述的问题对于任何单页应用程序都是通用的。

在本视频中简要讨论了它们: https://www.youtube.com/watch?v=IrgHurBjQbg 和中继(https://facebook.github.io/relay(是由Facebook开发的,以克服您所描述的权衡。

Relay的方法非常以数据为中心。它是对以下问题的答案:"如何在对服务器的一个查询中仅获取此视图中每个组件的所需数据?同时,Relay 可确保在多个视图中使用组件时,代码之间的耦合很小。

如果 Relay 不是一种选择,那么对于您描述的情况,"所有实体组件都读取自己的数据"对我来说似乎是一种更好的方法。我认为Flux中的误解是商店是什么。商店的概念不是保存模型或对象集合的地方。存储是应用程序在呈现视图之前放置数据的临时位置。它们存在的真正原因是解决不同存储中的数据之间的依赖关系问题。

Flux没有具体说明商店与模型和对象集合的概念(如Backbone(的关系。从这个意义上说,有些人实际上是在制作一个 flux 存储的地方,在其中放置特定类型的对象集合,这些对象在用户保持浏览器打开的整个过程中都不会刷新,但据我了解 flux,这不是存储应该的样子。

解决方案是使用另一个层,在其中存储和更新呈现视图(可能更多(所需的实体。如果你这个层抽象模型和集合,如果你的子组件必须再次查询才能得到自己的数据,那就不是问题了。