组件应在什么嵌套级别从Flux商店读取实体?


89

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

我一直在努力在组件层次结构中的什么地方应该从商店读取数据。
我尝试了两种极端的方法,但我都不喜欢:

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

需要从商店中获取一些数据的每个组件仅接收实体ID,并自行检索实体。
例如,Article被传递articleIdUserAvatarUserLink传递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可能有几个Categorys。

因此,如果我想以的级别从商店中检索所有数据,则Article需要:

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

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

加起来

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

我的目标:

  • 商店不应有疯狂的订户数量。如果父组件已经这样做,每个UserLink人都听不懂UserStore

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

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


1
感谢您发布此信息。我刚刚在这里发布了一个非常类似的问题:groups.google.com/forum / ...我所看到的所有内容都涉及平面数据结构而不是关联数据。我很惊讶没有看到任何最佳实践。
uberllama

Answers:


37

大多数人都是从在层次结构顶部附近的控制器视图组件中收听相关商店开始的。

后来,当似乎许多无关的道具通过层次结构向下传递到某个深层嵌套的组件时,有些人会决定让一个更深的组件侦听商店中的更改是个好主意。这样可以更好地封装问题域,而组件树的这个更深层次的分支就是这个问题域。要明智地进行此操作,有很多理由。

但是,我更喜欢总是在最上面听,并且简单地传递所有数据。有时,我什至会获取存储的整个状态,并将其作为单个对象向下传递到层次结构中,而我将对多个存储执行此操作。因此,我将为ArticleStore的状态提供一个支持,为的状态提供另一个支持UserStore,等等。我发现避免深度嵌套的控制器视图将为数据保留单个入口,并统一数据流。否则,我将有多个数据源,并且这将变得难以调试。

使用这种策略进行类型检查比较困难,但是您可以使用React的PropTypes为“按对象放置大型对象”设置“形状”或类型模板。参见:https : //github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -验证

请注意,您可能需要在商店本身中放置在商店之间关联数据的逻辑。所以,你的ArticleStore实力waitFor()UserStore,并且包括与每一个相关用户Article提供通过记录getArticles()。在您的视图中执行此操作听起来就像将逻辑推入视图层,这是您应尽可能避免的一种做法。

您可能也很想使用transferPropsTo(),并且很多人都喜欢这样做,但是我更喜欢保持所有内容的明确性,以提高可读性,从而保持可维护性。

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


4
感谢您的详细回答!我确实考虑过传递存储状态的两个快照。但是,这可能会给我带来麻烦,因为UserStore的每次更新都会导致屏幕上的所有Articles都被重新呈现,而PureRenderMixin无法判断需要更新哪些文章,不需要更新。至于您的第二个建议,我也曾考虑过,但我看不到如果在每次getArticles()调用中“注入”文章的用户,如何保持文章引用相等。这将再次给我带来性能问题。
Dan Abramov 2014年

1
我明白你的意思,但是有解决方案。例如,您可以签入shouldComponentUpdate(),查看文章的用户ID数组是否已更改,这基本上只是在这种情况下手动滚动您在PureRenderMixin中真正想要的功能和行为。
fisherwebdev

3
对了,反正我只需要做到这一点时,我敢肯定有这不应该是专卖店的情况是每分钟更新几次最大PERF的问题。再次感谢你!
Dan Abramov 2014年

8
根据您所构建的应用程序的类型,一旦您在屏幕上开始拥有更多不直接属于同一根目录的“小部件”,保留一个单一的入口点就会受到损害。如果您强行尝试这样做,则该根节点将承担太多责任。KISS和SOLID,即使它是客户端。
Rygu 2014年

37

我到达的方法是让每个组件都以道具的形式接收其数据(而不是ID)。如果某些嵌套组件需要相关实体,则取决于父组件来检索它。

在我们的示例中,Article应该有一个articleprop,它是一个对象(大概是由ArticleList或检索到的ArticlePage)。

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

重申:

  • 没有组件会收到ID作为道具。所有组件都接收各自的对象。
  • 如果子组件需要一个实体,则由家长负责检索它并作为道具传递。

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

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

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


只是为了在此答案之上添加一个角度。从概念上讲,您可以提出所有权的概念,因为数据收集将在实际拥有数据的最顶层视图中最终确定(因此将监听其存储)。几个例子:1. AuthData应该归App Component所有,Auth的任何更改都应作为prop传播给App Component的所有子项。2. ArticleData应该由ArticlePage或Article List拥有,如答案中所建议。现在,ArticleList的任何子项都可以从ArticleList接收AuthData和/或ArticleData,无论实际上是哪个组件提取了该数据。
Saumitra R. Bhave 2015年

2

我的解决方案要简单得多。每个具有自己状态的组件都可以交谈和收听商店。这些是非常类似于控制器的组件。不允许嵌套更深层的嵌套组件,这些组件不能保持状态,而只能渲染内容。他们仅收到用于纯渲染的道具,非常类似于视图。

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

在您的情况下,Article将是有状态的,因此仅与商店和UserLink等进行对话,因此它将接收article.user作为道具。


1
我想如果文章具有用户对象属性,这将起作用,但是在我的情况下,它仅具有userId(因为真实用户存储在UserStore中)。还是我想念你的意思?你会分享一个例子吗?
Dan Abramov 2014年

在文章中为用户启动访存。或更改您的API后端,以使用户ID“可扩展”,这样您就可以立即获取用户。两种方式都可以使更深层的嵌套组件保持简单视图,并且仅依赖于prop。
Rygu 2014年

2
我无力启动本文中的提取操作,因为它需要一起呈现。对于可扩展的API,我们曾经拥有它们,但是从商店解析它们非常困难。出于这个原因,我什至写了一个库来整理响应。
Dan Abramov 2014年

0

您的2种方法中描述的问题对于任何单页应用程序都是常见的。

在此视频中简要讨论了它们:https : //www.youtube.com/watch?v=IrgHurBjQbg和Relay(https://facebook.github.io/relay)是Facebook开发的,目的是克服您描述的折衷。

中继的方法非常以数据为中心。它是对以下问题的答案:“如何通过对服务器的一次查询,仅获取该视图中每个组件所需的数据?” 同时,Relay确保在多个视图中使用组件时,代码之间几乎没有耦合。

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

Flux并未指定商店与模型和对象集合(如骨干网)的关系。从某种意义上说,实际上是有人在一个流量存储中放置一个特定类型的对象的集合,该对象的收集在用户保持浏览器打开的整个时间中并不齐平,但是据我所知,这并不是商店应该是。

解决方案是在您的另一层上存储呈现视图(可能还有更多)所需的实体并保持更新。如果您是抽象模型和集合的这一层,那么子组件必须再次查询以获取自己的数据就不成问题。


1
据我了解,Dan保留不同类型的对象并通过id引用它们的方法使我们能够保留这些对象的缓存并仅在一个地方进行更新。例如,如果您有几篇引用同一用户的文章,然后更新了用户名,该如何实现?唯一的方法是使用新的用户数据更新所有文章。在后端选择文档还是关系数据库时,这与您遇到的问题完全相同。您对此有何看法?谢谢。
Alex Ponomarev 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.