5

目前正在做一个 react + redux 项目。

我还使用normalizr来处理数据结构并重新选择以收集应用程序组件的正确数据。

一切似乎运作良好。

我发现自己处于叶子状组件需要来自商店的数据的情况,因此我需要connect()组件来实现它。

作为一个简化的示例,假设该应用程序是一个书籍编辑系统,有多个用户收集反馈。

Book
    Chapters
        Chapter
            Comments
        Comments
    Comments

在应用程序的不同级别,用户可以对内容做出贡献和/或提供评论。

考虑我正在渲染一个章节,它有内容(和作者)和评论(每个都有自己的内容和作者)。

目前我会根据IDconnect()和章节内容。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()到商店以呈现反模式的标志?

我是在做正确的事,还是以错误的方式看待这件事?

4

3 回答 3

11

我认为你的直觉是正确的。只要 API 有意义,在任何级别(包括叶节点)连接组件都没有错——也就是说,给定一些道具,您可以推断组件的输出。

智能组件与愚蠢组件的概念有点过时了。相反,最好考虑连接组件与未连接组件。在考虑是否创建连接组件和未连接组件时,需要考虑一些事项。

模块边界

如果您将应用程序划分为更小的模块,通常最好将它们的交互限制在一个小的 API 表面上。例如,说usersandcomments位于单独的模块中,那么我会说<Comment>组件使用连接的<User id={comment.userId}/>组件而不是让它自己获取用户数据更有意义。

单一职责原则

承担过多责任的连接组件是代码异味。例如,<Comment>组件的职责可以是获取评论数据,渲染它,并以动作调度的形式处理用户交互(与评论)。如果它需要处理抓取用户数据和处理与用户模块的交互,那么它做的太多了。最好将相关职责委托给另一个连接的组件。

这也被称为“脂肪控制器”问题。

表现

通过在顶部有一个大的连接组件将数据向下传递,它实际上会对性能产生负面影响。这是因为每次状态更改都会更新顶级引用,然后每个组件都会重新渲染,React 需要对所有组件进行协调。

Redux 通过假设它们是纯的来优化连接的组件(即如果 prop 引用相同,则跳过重新渲染)。如果你连接叶子节点,那么状态的改变只会重新渲染受影响的叶子节点——跳过很多协调。这可以在这里看到:https ://github.com/mweststrate/redux-todomvc/blob/master/components/TodoItem.js

重用和可测试性

我要提到的最后一件事是重用和测试。如果您需要 1) 将其连接到状态原子的另一部分,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'
于 2016-05-10T15:10:49.190 回答
1

我同意@Kevin He 的回答,即这并不是真正的反模式,但通常有更好的方法可以让您的数据流更容易追踪。

为了在不连接叶状组件的情况下完成您的目标,您可以调整选择器以获取更完整的数据集。例如,对于您的<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 />组件都有自己的记忆选择器,以防你有多个。(多个不同<Chapter />的组件共享同一个选择器会破坏记忆)。我还拆分chapterCommentsSelector为一个单独的重新选择选择器,以便对其进行记忆,因为它会转换(在本例中为过滤器)来自状态的数据。

在您的<Chapter />组件中,您可以调用createChapterDataSelector(),这将为您提供一个选择器,该选择器提供一个不可变映射,其中包含您需要的所有数据<Chapter />及其所有后代。然后你可以简单地将道具正常传递下去。

以正常的 React 方式传递 props 的两个主要好处是可追溯的数据流和组件的可重用性。<Comment />通过 'content'、'authorName' 和 'authorAvatar' 属性进行渲染的组件易于理解和使用。您可以在您想要显示评论的应用程序中的任何位置使用它。想象一下,您的应用程序在编写评论时显示评论的预览。使用“哑”组件,这是微不足道的。但是如果你的组件需要在你的 Redux 存储中匹配实体,这是一个问题,因为如果该注释仍在编写中,存储中可能还不存在。

connect()但是,可能会有一段时间,对更远的组件更有意义。一个强有力的例子是,如果你发现你通过不需要它们的中间人组件传递了大量的道具,只是为了让它们到达最终目的地。

来自 Redux 文档:

尝试将您的演示文稿组件分开。在方便时通过连接它们来创建容器组件。每当您觉得您在父组件中复制代码以为相同类型的子组件提供数据时,是时候提取一个容器了。通常,一旦您觉得父母对“个人”数据或其子女的行为了解太多,就该提取容器了。一般来说,尝试在可理解的数据流和组件的责任范围之间找到平衡。

推荐的方法似乎是从较少连接的容器组件开始,然后只在需要时提取更多容器。

于 2016-05-09T08:41:54.220 回答
0

Redux 建议您只将上层容器连接到 store。你可以从容器中传递你想要的叶子的每个道具。这样,更容易追踪数据流。

这只是个人喜好的事情,将类叶子组件连接到商店并没有错,它只是给你的数据流增加了一些复杂性,从而增加了调试的难度。

如果您发现在您的应用程序中,将类似叶子的组件连接到商店要容易得多,那么我建议您这样做。但它不应该经常发生。

于 2016-05-09T08:03:41.313 回答