在什么嵌套级别上,组件应该从 Stores in Flux 读取实体?

我重写我的应用程序使用通量和我有一个问题,检索数据从商店。我有很多组件,它们有很多巢。它们有的大(Article) ,有的小而简单(UserAvatarUserLink)。

我一直在纠结在组件层次结构中我应该在哪里读取来自 Stores 的数据。
我尝试了两种极端的方法,两种我都不太喜欢:

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

每个需要从 Store 获得一些数据的组件只接收实体 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 传递给子实体,子实体将再次检索该实体(否则将破坏一致性)。

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

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

例如:

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

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

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

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

总结

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

我的目标:

  • 商店不应该有这么多疯狂的用户。如果父组件已经在监听 UserStore,那么每个 UserLink监听 UserStore就是愚蠢的。

  • 如果父组件已经从存储中获取了一些对象(例如 user) ,我不希望任何嵌套组件必须再次获取它。我应该可以通过道具传球。

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

15696 次浏览

The approach at which I arrived is having each components receive its data (not IDs) as a prop. If some nested component needs a related entity, it's up to the parent component to retrieve it.

In our example, Article should have an article prop which is an object (presumably retrieved by ArticleList or ArticlePage).

Because Article also wants to render UserLink and UserAvatar for article's author, it will subscribe to UserStore and keep author: UserStore.get(article.authorId) in its state. It will then render UserLink and UserAvatar with this this.state.author. If they wish to pass it down further, they can. No child components will need to retrieve this user again.

To reiterate:

  • No component ever receives ID as a prop; all components receive their respective objects.
  • If child components needs an entity, it's parent's responsibility to retrieve it and pass as a prop.

This solves my problem quite nicely. Code example rewritten to use this approach:

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

This keeps innermost components stupid but doesn't force us to complicate the hell out of top level components.

My solution is much simpler. Every component that has its own state is allowed to talk and listen to stores. These are very controller-like components. Deeper nested components that don't maintain state but just render stuff aren't allowed. They only receive props for pure rendering, very view-like.

This way everything flows from stateful components into stateless components. Keeping the statefuls count low.

In your case, Article would be stateful and therefore talks to the stores and UserLink etc. would only render so it would receive article.user as prop.

Most people start out by listening to the relevant stores in a controller-view component near the top of the hierarchy.

Later, when it seems like a lot of irrelevant props are getting passed down through the hierarchy to some deeply nested component, some people will decided it's a good idea to let a deeper component listen for changes in the stores. This offers a better encapsulation of the problem domain that this deeper branch of the component tree is about. There are good arguments to be made for doing this judiciously.

However, I prefer to always listen at the top and simply pass down all the data. I will sometimes even take the entire state of the store and pass it down through the hierarchy as a single object, and I will do this for multiple stores. So I would have a prop for the ArticleStore's state, and another for the UserStore's state, etc. I find that avoiding deeply nested controller-views maintains a singular entry point for the data, and unifies the data flow. Otherwise, I have multiple sources of data, and this can become difficult to debug.

Type checking is more difficult with this strategy, but you can set up a "shape", or type template, for the large-object-as-prop with React's PropTypes. See: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop-validation

Note that you may want to put the logic of associating data between stores in the stores themselves. So your ArticleStore might waitFor() the UserStore, and include the relevant Users with every Article record it provides through getArticles(). Doing this in your views sounds like pushing logic into the view layer, which is a practice you should avoid whenever possible.

You might also be tempted to use transferPropsTo(), and many people like doing this, but I prefer to keep everything explicit for readability and thus maintainability.

FWIW, my understanding is that David Nolen takes a similar approach with his Om framework (which is somewhat Flux-compatible) with a single entry point of data on the root node -- the equivalent in Flux would be to only have one controller-view listening to all stores. This is made efficient by using shouldComponentUpdate() and immutable data structures that can be compared by reference, with ===. For immutable data structures, checkout David's mori or Facebook's immutable-js. My limited knowledge of Om primarily comes from The Future of JavaScript MVC Frameworks

The problems described in your 2 philosophies are common to any single page application.

They are discussed briefly in this video: https://www.youtube.com/watch?v=IrgHurBjQbg and Relay ( https://facebook.github.io/relay ) was developed by Facebook to overcome the tradeoff that you describe.

Relay's approach is very data centric. It is an answer to the question "How do I get just the needed data for each components in this view in one query to the server?" And at the same time Relay makes sure that you have little coupling across the code when a component used in multiple views.

If Relay is not an option, "All entity components read their own data" seems a better approach to me for the situation you describe. I think the misconception in Flux is what a store is. The concept of store exist no to be the place where a model or a collection of objects are kept. Stores are temporary places where your application put the data before the view is rendered. The real reason they exist is to solve the problem of dependencies across the data that goes in different stores.

What Flux is not specifying is how a store relate to the concept of models and collection of objects (a la Backbone). In that sense some people are actually making a flux store a place where to put collection of objects of a specific type that is not flush for the whole time the user keeps the browser open but, as I understand flux, that is not what a store is supposed to be.

The solution is to have another layer where you where the entities necessary to render your view (and potentially more) are stored and kept updated. If you this layer that abstract models and collections, it is not a problem if you the subcomponents have to query again to get their own data.