组件应该在哪个嵌套级别从 Flux 中的 Stores 读取实体?

IT技术 javascript reactjs reactjs-flux
2021-03-28 07:37:10

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

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

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

每个需要来自 Store 的数据的组件只接收实体 ID 并自行检索实体。
例如,Article被通过articleIdUserAvatar并且UserLink被通过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 个组件可能订阅 Stores 令人沮丧;
  • 这是很难掌握的数据如何被更新,以什么顺序,因为每个组件独立获取数据;
  • 即使您可能已经有一个处于状态的实体,您也必须将其 ID 传递给子代,子代将再次检索它(否则会破坏一致性)。

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

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

例如:

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

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

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

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

加起来

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

我的目标:

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

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

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

4个回答

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

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

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

这种策略的类型检查更加困难,但您可以使用 React 的 PropTypes 为 large-object-as-prop 设置“形状”或类型模板。请参阅: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 框架的未来

根据您正在构建的应用程序类型,一旦您开始在屏幕上有更多不直接属于同一根的“小部件”,保持一个单一的入口点就会受到伤害。如果您强行尝试这样做,则该根节点将承担太多责任。KISS 和 SOLID,即使它是客户端。
2021-05-23 07:37:10
我明白你的意思,但有解决方案。首先,如果文章的用户 ID 数组发生了变化,您可以检查 shouldComponentUpdate(),这基本上只是在这种特定情况下手动滚动 PureRenderMixin 中您真正想要的功能和行为。
2021-06-02 07:37:10
对了,反正我只需要做到这一点时,我敢肯定有这不应该是专卖店的情况是每分钟更新几次最大PERF的问题。再次感谢你!
2021-06-02 07:37:10
谢谢详细解答!我确实考虑过传递商店状态的两个完整快照。然而,这可能会给我带来性能问题,因为 UserStore 的每次更新都会导致屏幕上的所有文章都被重新渲染,而 PureRenderMixin 将无法判断哪些文章需要更新,哪些不需要。至于你的第二个建议,我也考虑过,但是如果在每个 getArticles() 调用中“注入”文章的用户,我不知道如何保持文章引用相等。这可能再次给我带来性能问题。
2021-06-10 07:37:10

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

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

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

重申:

  • 没有任何组件将 ID 作为props接收;所有组件都接收它们各自的对象。
  • 如果子组件需要一个实体,父组件有责任检索它并作为props传递。

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

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 组件中拥有 Auth 中的任何更改都应该作为道具向下传播到 App 组件的所有子组件。2.按照回答中的建议,文章数据应归文章页面或文章列表所有。现在,ArticleList 的任何子项都可以从 ArticleList 接收 AuthData 和/或 ArticleData,而不管哪个组件实际获取了该数据。
2021-06-16 07:37:10

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

通过这种方式,一切都从有状态组件流向无状态组件。保持有状态的计数很低。

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

为文章中的用户启动提取。或者更改您的 API 后端以使用户 ID“可扩展”,以便您立即获取用户。无论哪种方式,都可以让更深层次的嵌套组件保持简单的视图,并且仅依赖于 props。
2021-05-23 07:37:10
我负担不起在文章中启动提取,因为它需要一起呈现。至于可扩展的 API,我们曾经有过,但是从 Stores 中解析它们是非常有问题的。出于这个原因,我什至写了一个库来扁平化响应。
2021-05-30 07:37:10
我想如果文章具有用户对象属性,这会起作用,但在我的情况下,它只有 userId(因为真正的用户存储在 UserStore 中)。还是我错过了你的观点?你会分享一个例子吗?
2021-06-11 07:37:10

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

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

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

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

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

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

据我了解,Dan 保留不同类型的对象并通过 id 引用它们的方法允许我们保留这些对象的缓存并仅在一个地方更新它们。如果比方说,您有几篇文章引用了同一个用户,然后更新了用户名,那么如何实现呢?唯一的方法是用新的用户数据更新所有文章。这与在后端的文档与关系数据库之间进行选择时遇到的问题完全相同。你对此有何看法?谢谢你。
2021-05-27 07:37:10