叶状组件中的 connect() 是 react+redux 中反模式的标志吗?

Is connect() in leaf-like components a sign of anti-pattern in react+redux?

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

我还使用 normalizr to handle the data structure and reselect 为应用程序组件收集正确的数据。

似乎一切正常。

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

举个简单的例子,假设该应用是一个有多个用户收集反馈的图书编辑系统。

Book
    Chapters
        Chapter
            Comments
        Comments
    Comments

在应用的不同级别,用户可以为内容做出贡献and/or提供评论。

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

目前我会根据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,并将其直接传递给用户 – 但这似乎只会让选择器变得非常复杂,而且因为我的数据是不可变的,所以它每次都会重新创建对象。

所以,我的问题是,将叶状组件(如上面的 User)connect() 存储到商店以呈现反模式的迹象吗?

我是在做正确的事,还是看错了?

Redux 建议您只将上层容器连接到商店。你可以从容器中传递你想要的每一个道具。这样更容易追溯数据流向

这只是个人喜好问题,把leaf-like组件连接到store并没有错,只是增加了你的数据流的复杂度,增加了debug的难度。

如果您发现在您的应用中,将叶状组件连接到商店要容易得多,那么我建议您这样做。但这种情况应该不会经常发生。

我同意 这并不是真正的反模式,但通常有更好的方法可以使您的数据流更易于跟踪。

要在不连接叶状组件的情况下完成您的目标,您可以调整选择器以获取更完整的数据集。例如,对于 <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'))
      }))
    })
  )
}

此示例使用一个函数,该函数 returns 专门针对给定章节 ID 的选择器,以便每个 <Chapter /> 组件都有自己的备忘选择器,以防您有多个选择器。 (多个不同的 <Chapter /> 组件共享同一个选择器会破坏记忆)。我还将 chapterCommentsSelector 拆分为一个单独的重新选择选择器,以便将其存储起来,因为它转换(在本例中为过滤器)来自状态的数据。

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

以正常的 React 方式传递 props 的两个主要好处是可追踪的数据流和组件的可重用性。通过 'content'、'authorName' 和 'authorAvatar' 属性渲染的 <Comment /> 组件很容易理解和使用。您可以在应用中任何要显示评论的地方使用它。想象一下,您的应用在撰写评论时显示评论的预览。使用 "dumb" 组件,这是微不足道的。但是,如果您的组件需要在您的 Redux 商店中匹配实体,那将是一个问题,因为如果该评论仍在写入,则该评论可能不存在于商店中。

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

来自 Redux 文档:

Try to keep your presentation components separate. Create container components by connecting them when it’s convenient. Whenever you feel like you’re duplicating code in parent components to provide data for same kinds of children, time to extract a container. Generally as soon as you feel a parent knows too much about “personal” data or actions of its children, time to extract a container. In general, try to find a balance between understandable data flow and areas of responsibility with your components.

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

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

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

模块边界

如果您将应用划分为更小的模块,通常最好将它们的交互限制在较小的 API 表面。例如,假设 userscomments 在不同的模块中,那么我会说 <Comment> 组件使用连接的 <User id={comment.userId}/> 组件比拥有它更有意义自己抓取用户数据。

单一职责原则

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

这也称为 "fat-controller" 问题。

性能

通过在顶部有一个大的连接组件向下传递数据,它实际上会对性能产生负面影响。这是因为每次状态更改都会更新顶级引用,然后每个组件都会重新渲染,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'