在leaf-like组件中connect()是react + redux中反模式的标志吗?

时间:2016-05-09 06:09:47

标签: javascript redux immutable.js react-redux normalizr

目前正在开展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()到商店呈现反模式的标志?

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

3 个答案:

答案 0 :(得分:9)

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

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

模块边界

如果您将应用划分为较小的模块,通常最好将其交互限制在较小的API表面。例如,假设userscomments位于不同的模块中,那么我会说<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建议您只将上层容器连接到商店。你可以从容器中传递你想要的每一个道具。通过这种方式,可以更轻松地跟踪数据流。

这只是个人偏好的事情,将类似叶子的组件连接到商店没有任何问题,它只会增加数据流的复杂性,从而增加调试的难度。

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