89

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

我一直在努力解决组件层次结构中应该从 Stores 读取数据的位置。
我尝试了两种极端的方法,但我都不太喜欢:

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

每个需要来自 Store 的数据的组件只接收实体 ID 并自行检索实体。
例如,被Article通过articleId,被通过。UserAvatarUserLinkuserId

这种方法有几个明显的缺点(在代码示例下讨论)。

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 多个可能订阅 Store 的组件令人沮丧;
  • 很难跟踪数据的更新方式和顺序,因为每个组件都独立检索其数据;
  • 即使您可能已经有一个实体处于状态,您也必须将其 ID 传递给孩子,孩子们将再次检索它(否则会破坏一致性)。

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

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

例如:

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

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

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

更令人沮丧的是,每当我需要一个深度嵌套的实体时,我都需要在每一层嵌套中添加代码以另外传递它。

加起来

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

我的目标:

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

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

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

4

4 回答 4

37

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

在我们的示例中,Article应该有一个article作为对象的道具(可能由ArticleListor检索ArticlePage)。

因为Article还要渲染UserLink并且UserAvatar对于文章的作者,它会订阅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>
    )
  }
});

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

于 2014-09-06T14:16:10.960 回答
37

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

后来,当看起来很多不相关的 props 似乎通过层次结构传递到一些深度嵌套的组件时,有些人会认为让更深层次的组件监听 store 中的变化是个好主意。这为组件树的这个更深分支所涉及的问题域提供了更好的封装。明智地这样做是有充分理由的。

但是,我更喜欢始终在顶部聆听并简单地传递所有数据。有时我什至会获取商店的整个状态并将其作为单个对象传递到层次结构中,并且我将为多个商店执行此操作。所以我会有一个用于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 的有限了解主要来自The Future of JavaScript MVC Frameworks

于 2014-09-07T08:45:04.500 回答
2

我的解决方案要简单得多。每个拥有自己状态的组件都可以与 store 对话和收听。这些是非常类似于控制器的组件。不允许更深层次的不维护状态但只渲染内容的嵌套组件。他们只接收纯渲染的道具,非常像视图。

这样,一切都从有状态组件流向无状态组件。保持有状态的数量很少。

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

于 2014-09-06T16:22:21.217 回答
0

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

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

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

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

Flux 没有具体说明 store 与模型和对象集合(a la Backbone)的概念之间的关系。从这个意义上说,有些人实际上是在让通量商店成为一个放置特定类型对象集合的地方,这些对象在用户保持浏览器打开的整个过程中都不会刷新,但是据我了解通量,这不是商店应该是。

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

于 2015-10-16T22:03:56.050 回答