我正在重写我的应用程序以使用Flux,我在从商店检索数据时遇到问题。我有很多组件,它们很多。其中一些很大(Article
),有些很小而且很简单(UserAvatar
,UserLink
)。
我一直在努力应对组件层次结构中的哪个位置,我应该从商店中读取数据 我尝试了两种极端方法,我都不喜欢这两种方法:
需要Store中某些数据的每个组件只接收实体ID并自行检索实体
例如,Article
传递articleId
,UserAvatar
和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>
)
}
});
这种方法的缺点:
当我厌倦了追踪错误时,我试图将所有数据检索放在最顶层。然而,事实证明这是不可能的,因为对于某些实体我有几层嵌套。
例如:
Category
包含UserAvatar
为该类别做出贡献的人员; Article
可能有多个Category
s。因此,如果我想在Article
级别从商店中检索所有数据,我需要:
ArticleStore
; CategoryStore
; UserStore
; 更令人沮丧的是,每当我需要一个深度嵌套的实体时,我需要在每个嵌套级别添加代码以进一步传递它。
这两种方法都有缺陷。我如何最优雅地解决这个问题?
我的目标:
商店不应该有疯狂的订阅者数量。如果父组件已经这样做,那么每个UserLink
听UserStore
都是愚蠢的。
如果父组件已从商店中检索了某个对象(例如user
),我不希望任何嵌套组件必须再次获取它。我应该可以通过道具传递它。
我不应该在顶层获取所有实体(包括关系),因为它会使添加或删除关系变得复杂。每次嵌套实体获得新关系时,我都不想在所有嵌套级别引入新道具(例如,类别获得curator
)。
答案 0 :(得分:36)
我到达的方法是让每个组件接收其数据(而不是ID)作为道具。如果某个嵌套组件需要相关实体,则由父组件来检索它。
在我们的示例中,Article
应该有一个article
道具,它是一个对象(可能由ArticleList
或ArticlePage
检索。)
由于Article
也希望为文章作者呈现UserLink
和UserAvatar
,因此会订阅UserStore
并保留author: UserStore.get(article.authorId)
州。然后,它会使用此UserLink
呈现UserAvatar
和this.state.author
。如果他们希望进一步下去,他们可以。没有子组件需要再次检索此用户。
重申:
这很好地解决了我的问题。重写代码示例以使用此方法:
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 :(得分:36)
大多数人都是通过在层次结构顶部附近的控制器视图组件中侦听相关存储来开始的。
后来,当看起来很多不相关的道具通过层次结构传递给一些深层嵌套的组件时,某些人会认为让更深层的组件听取更改是个好主意在商店里。这为组件树的这个更深层次的分支提供了更好的问题域封装。明智地做这件事有很好的理由。
但是,我更喜欢总是在顶部听,简单地传递所有数据。我有时甚至会将整个商店状态作为单个对象传递给层次结构,我将为多个商店执行此操作。所以我有一个支持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-validation
请注意,您可能希望在商店本身的商店之间建立关联数据的逻辑。因此,ArticleStore
可能会waitFor()
UserStore
,并通过Article
向相关用户提供其提供的每个getArticles()
记录。在您的视图中执行此操作听起来像将逻辑推入视图层,这是您应该尽可能避免的做法。
您可能也很想使用transferPropsTo()
,很多人喜欢这样做,但我更倾向于将所有内容都保持清晰,以便于阅读和维护。
FWIW,我的理解是David Nolen对他的Om框架(somewhat Flux-compatible)采用了类似的方法,在根节点上有一个数据入口点 - Flux中的等价物只有一个控制器视图监听所有商店。通过使用shouldComponentUpdate()
和可通过引用进行比较的不可变数据结构(===),可以提高效率。对于不可变数据结构,请查看David的mori或Facebook的immutable-js。我对Om的有限知识主要来自The Future of JavaScript MVC Frameworks
答案 2 :(得分:1)
我的解决方案更简单。每个拥有自己状态的组件都可以与商店交谈和收听。这些是类似控制器的组件。更深层次的嵌套组件不会保持状态但只是渲染内容是不允许的。它们只接收纯渲染的道具,非常像视图。
这样一切都从有状态组件流入无状态组件。保持有状态的数量很低。
在您的情况下,文章将是有状态的,因此与商店和UserLink等的谈话只会呈现,因此它将接收article.user作为道具。
答案 3 :(得分:0)
您的2个哲学中描述的问题对于任何单页应用程序都是通用的。
本视频简要讨论了它们:https://www.youtube.com/watch?v=IrgHurBjQbg和Relay(https://facebook.github.io/relay)由Facebook开发,以克服您所描述的权衡。
Relay的方法以非常以数据为中心。这是对“如何在一次查询服务器中获取此视图中每个组件所需数据?”这一问题的答案。同时,当在多个视图中使用组件时,Relay确保您在代码中几乎没有耦合。
如果Relay不是一个选项,“所有实体组件读取他们自己的数据”似乎对我描述的情况更好。 我认为Flux中的误解就是商店。存储的概念不存在于保持模型或对象集合的地方。存储是应用程序在呈现视图之前放置数据的临时位置。它们存在的真正原因是解决了不同商店中数据之间的依赖关系问题。
Flux未指定的是商店如何与模型概念和对象集合(la Backbone)相关联。 从某种意义上说,有些人实际上是在一个磁通存储器中放置一个特定类型的对象集合的地方,这个对象在用户保持浏览器打开的整个时间内都不是齐平的,但据我所知,这不是商店应该是。
解决方案是拥有另一个图层,您可以在其中存储渲染视图所需的实体(可能还有更多),并保持更新。如果您是抽象模型和集合的此层,那么如果子组件必须再次查询以获取自己的数据,则不会出现问题。