目前正在开展react + redux项目。
我还使用normalizr来处理数据结构,并使用reselect来收集应用组件的正确数据。
所有似乎都运作良好。
我发现自己处于类似叶子的组件需要来自商店的数据的情况,因此我需要connect()
组件来实现它。
作为一个简化示例,假设该应用程序是一个图书编辑系统,有多个用户收集反馈。
Book
Chapters
Chapter
Comments
Comments
Comments
在应用的不同级别,用户可以贡献内容和/或提供评论。
考虑我渲染一个章节,它有内容(和作者)和评论(每个都有自己的内容和作者)。
目前我会根据ID connect()
和reselect
章节内容。
因为数据库是用normalizr规范化的,所以我真的只得到章节的基本内容字段和作者的用户ID。
为了呈现注释,我将使用一个可以重新选择链接到章节的注释的连接组件,然后单独呈现每个注释组件。
同样,因为数据库是用normalizr规范化的,所以我真的只得到评论作者的基本内容和用户ID。
现在,为了呈现像作者徽章一样简单的东西,我需要使用另一个连接的组件从我拥有的用户ID中获取用户详细信息(在呈现章节作者和每个评论作者时)。
组件会像这样简单:
@connect(
createSelector(
(state) => state.entities.get('users'),
(state,props) => props.id,
(users,id) => ( { user:users.get(id)})
)
)
class User extends Component {
render() {
const { user } = this.props
if (!user)
return null
return <div className='user'>
<Avatar name={`${user.first_name} ${user.last_name}`} size={24} round={true} />
</div>
}
}
User.propTypes = {
id : PropTypes.string.isRequired
}
export default User
它看似很好。
我尝试反过来并在较高级别将数据反规范化,以便例如章节数据直接嵌入用户数据,而不仅仅是用户ID,并直接将其传递给用户 - 但这似乎只是制作非常复杂的选择器,并且因为我的数据是不可变的,所以每次都只重新创建对象。
所以,我的问题是,是否有类似叶子的组件(如上面的用户)connect()
到商店呈现反模式的标志?
我做的是正确的,还是以错误的方式看待这个?
答案 0 :(得分:9)
我认为你的直觉是正确的。在任何级别(包括叶节点)连接组件都没有错,只要API有意义 - 也就是说,给定一些道具可以推断组件的输出。
智能与愚蠢组件的概念有点过时了。相反,最好考虑连接组件与未连接组件。在考虑是否创建连接组件与未连接组件时,需要考虑一些事项。
如果您将应用划分为较小的模块,通常最好将其交互限制在较小的API表面。例如,假设users
和comments
位于不同的模块中,那么我会说<Comment>
组件使用连接的<User id={comment.userId}/>
组件而不是使用它更有意义抓住用户数据。
负责太多的连接组件是代码味道。例如,<Comment>
组件的职责可以是获取注释数据并进行呈现,并以操作调度的形式处理用户交互(与注释一起)。如果它需要处理抓取用户数据,并处理与用户模块的交互,那么它做得太多了。最好将相关职责委托给另一个连接组件。
这也被称为“脂肪控制器”问题。
通过在顶部放置一个大型连接组件来传递数据,它实际上会对性能产生负面影响。这是因为每次状态更改都会更新顶级引用,然后每个组件都会重新呈现,而React将需要对所有组件执行协调。
Redux通过假设它们是纯粹的(即如果prop引用是相同的,则跳过重新渲染)来优化连接的组件。如果连接叶节点,则状态更改将仅重新呈现受影响的叶节点 - 跳过大量协调。这可以在这里看到:https://github.com/mweststrate/redux-todomvc/blob/master/components/TodoItem.js
我要提到的最后一件事是重用和测试。如果你需要1)将它连接到state atom的另一部分,2)直接传入数据(例如我已经有user
数据,那么连接组件是不可重用的,所以我只想要一个纯渲染)。同样,连接的组件更难测试,因为您需要先设置它们的环境才能渲染它们(例如创建存储,将存储传递给<Provider>
等)。
通过在有意义的地方导出已连接和未连接的组件,可以缓解此问题。
export const Comment = ({ comment }) => (
<p>
<User id={comment.userId}/>
{ comment.text }
</p>
)
export default connect((state, props) => ({
comment: state.comments[props.id]
}))(Comment)
// later on...
import Comment, { Comment as Unconnected } from './comment'
答案 1 :(得分:1)
我同意@Kevin He's answer它不是真正的反模式,但通常有更好的方法可以让您的数据流更容易追踪。
要在不连接类似叶子的组件的情况下完成您的目标,您可以调整选择器以获取更完整的数据集。例如,对于<Chapter/>
容器组件,您可以使用以下内容:
export const createChapterDataSelector = () => {
const chapterCommentsSelector = createSelector(
(state) => state.entities.get('comments'),
(state, props) => props.id,
(comments, chapterId) => comments.filter((comment) => comment.get('chapterID') === chapterId)
)
return createSelector(
(state, props) => state.entities.getIn(['chapters', props.id]),
(state) => state.entities.get('users'),
chapterCommentsSelector,
(chapter, users, chapterComments) => I.Map({
title: chapter.get('title'),
content: chapter.get('content')
author: users.get(chapter.get('author')),
comments: chapterComments.map((comment) => I.Map({
content: comment.get('content')
author: users.get(comment.get('author'))
}))
})
)
}
此示例使用一个函数,该函数返回一个专门用于给定章节ID的选择器,以便每个<Chapter />
组件获得自己的memoized选择器,以防您有多个。 (共享相同选择器的多个不同<Chapter />
组件会破坏memoization)。我还将chapterCommentsSelector
拆分为一个单独的重选选择器,以便它被记忆,因为它会转换(在这种情况下过滤器)来自状态的数据。
在<Chapter />
组件中,您可以拨打createChapterDataSelector()
,它会为您提供一个选择器,该选择器提供一个不可变的地图,其中包含您<Chapter />
所需的所有数据以及所有数据它的后代。然后你可以简单地将道具正常传递下来。
以正常的React方式传递props的两个主要好处是可跟踪数据流和组件可重用性。通过'content','authorName'和'authorAvatar'道具进行渲染的<Comment />
组件易于理解和使用。您可以在应用中的任何位置使用它来显示评论。想象一下,您的应用会显示正在撰写的评论预览。使用“哑”组件,这是微不足道的。但是如果你的组件在Redux商店中需要一个匹配的实体,那就是一个问题,因为如果它仍然被写入,那么商店中可能不存在这个注释。
但是,有时候connect()
组件更有意义。一个强有力的理由是,如果你发现你通过不需要它们的中间人组件传递大量道具,只是为了让它们到达最终目的地。
来自Redux文档:
尝试将您的演示文稿组件分开。创建容器 组件在方便时连接它们。每当你感觉到 就像你在父组件中复制代码来提供数据一样 同类儿童,抽出容器的时间。一般来说很快 因为你觉得父母对“个人”数据或行为了解得太多 它的孩子,抽出容器的时间。一般来说,试着找 在可理解的数据流和责任范围之间取得平衡 与您的组件。
推荐的方法似乎是从较少的连接容器组件开始,然后在需要时只提取更多容器。
答案 2 :(得分:0)
Redux建议您只将上层容器连接到商店。你可以从容器中传递你想要的每一个道具。通过这种方式,可以更轻松地跟踪数据流。
这只是个人偏好的事情,将类似叶子的组件连接到商店没有任何问题,它只会增加数据流的复杂性,从而增加调试的难度。
如果您在应用中发现这一点,将类似叶子的组件连接到商店要容易得多,那么我建议您这样做。但它不应该经常发生。