【译】React 容器组件和展示组件相分离的思想

1,895 阅读7分钟

转载自 Dan Abramov. Presentational and Container Components. Mar 23, 2015

2019年更新:我在很久以前写了这篇文章,但后来我的观点发生了变化。特别是,我不建议再像这样拆分你的组件。如果你觉得在你的代码库中这样写很自然,这种模式可以很方便。但我已经看到它在没有任何必要的情况下被强制执行,而且几乎是教条式的狂热,次数太多了。我发现它有用的主要原因是,它让我把复杂的有状态逻辑从组件的其他方面分离出来。Hooks让我做了同样的事情而且没有任意的划分。由于历史原因,我完整地保留了这段文字,但不要太当真。

在编写React应用时,有一个简单的模式我觉得非常有用。如果你已经做了一段时间的React,你可能已经发现了它。这篇文章很好地解释了它,但我想再补充几点。 如果你把你的组件分为两类,你会发现你的组件更容易重用和推理。我把它们称为容器Container展示Presentational型组件*,但我也听说过 Fat 和 Skinny,Smart 和 Dumb,Stateful和 Pure,Screen 和 Component等。这些都不完全相同,不过核心思想是相似的。

我对展示型组件的理解:
  • 关注的是事物的外观。
  • 内部可能同时包含展示和容器组件**,通常有自己的一些DOM标记和样式。
  • 通常允许通过this.props.children进行包含。
  • 对应用的其他部分没有依赖性,比如Flux动作或商店。
  • 不要指定数据的加载或突变方式。
  • 只通过props来接收数据和回调。
  • 很少有自己的状态(即使有,也是UI状态而不是数据)。
  • 除非它们需要状态、生命周期钩子或性能优化,否则应被写成函数组件(functional components)。
  • 例如:页面、边栏、故事、用户信息、列表。
我对容器型组件的理解:
  • 关注的是事物如何工作。
  • 内部可能同时包含展示和容器组件**,但除了一些包装div外,通常没有自己的任何DOM标记,也从来没有任何样式。 为presentational或其他容器组件提供数据和行为。
  • 调用 Flux 动作,并将这些动作作为回调提供给展示组件。
  • 通常是有状态的,因为它们往往作为数据源。
  • 通常使用更高阶的组件生成,如React Redux的connect()、Relay的createContainer()或Flux Utils的Container.create(),而不是手工编写。
  • 例如:UserPage, FollowersSidebar, StoryContainer, FollowedUserList.

我把它们放在不同的文件夹里,以明确这种区别。

这种方法的好处
  • 更好的分离关注点。通过这种方式编写组件,您可以更好地理解您的应用程序和您的UI。
  • 更好的重用性。你可以用完全不同的状态源来使用相同的表现型组件,并将其转化为可以进一步重用的独立容器组件。 展示型组件本质上就是你的应用的 "调色板"。你可以把它们放在一个页面上,让设计师调整它们的所有变化,而不触及应用的逻辑。你可以在该页面上运行截图回归测试。
  • 这迫使你提取 "布局组件",如Sidebar,Page,ContextMenu,并使用this.props.children,而不是在几个容器组件中重复相同的标记和布局。

请记住,组件不一定要操作DOM。它们只需要提供UI关注点之间的组成边界。 利用好这一点。

什么时候引入容器?

我建议你先只用展示组件开始构建你的应用。最终你会意识到,你把太多的道具传递给了中间的组件。当你注意到有些组件并没有使用它们接收到的道具,而只是将它们转发下去,而且当子组件需要更多数据时,你必须随时重新连接所有这些中间组件,这就是引入一些容器组件的好时机。这样你就可以把数据和行为道具送到叶子组件上,而不会给树中间的不相关组件造成负担。 这是一个不断重构的过程,所以不要试图第一次就把它做好。当你尝试这种模式时,你会对什么时候该提取一些容器有一种直观的感觉,就像你知道什么时候该提取一个函数一样。我的free Redux Egghead系列或许也能帮到你!

其他划分法

重要的是,你要明白,展示性组件和容器之间的区别不是技术上的区别。而是它们的目的的区别。 相比之下,这里有几个相关(但不同!)的技术区别。

  • 有状态和无状态。有些组件使用React setState()方法,有些不使用。虽然容器组件倾向于有状态,而呈现型组件倾向于无状态,但这并不是一个硬性规则。呈现型组件可以是有状态的,容器也可以是无状态的。

  • 类和函数。从React 0.14开始,组件既可以声明为类,也可以声明为函数。函数式组件的定义更简单,但它们缺乏某些目前只有类组件才有的功能。其中一些限制可能会在未来消失,但它们今天仍然存在。因为功能组件更容易理解,我建议你使用它们,除非你需要状态、生命周期钩子或性能优化,这些功能目前只有类组件才有。

  • 纯粹和不纯粹。人们说,如果一个组件能保证在相同的道具和状态下返回相同的结果,那么这个组件就是纯粹的。纯组件既可以定义为类,也可以定义为函数,既可以有状态,也可以无状态。纯组件的另一个重要方面是它们不依赖于道具或状态的深度突变,因此它们的渲染性能可以通过在其shouldComponentUpdate()钩子中进行浅层比较来优化。目前只有类可以定义shouldComponentUpdate(),但未来可能会改变。

呈现型组件和容器都可以归入这两种类型。根据我的经验,呈现型组件往往是无状态的纯函数,而容器往往是有状态的纯类。然而这并不是一个规则,而是一个观察结果,我也看到过在特定情况下完全相反的情况,这是有道理的。 不要把有状态和容器组件分离当成教条。有时候这并不重要,或者说很难划清界限。如果你觉得不确定某个特定的组件应该是呈现性的还是容器的,那么现在决定可能还为时过早。不要紧张!

Example

This gist by Michael Chan pretty much nails it.

Further Reading

Getting Started with Redux Mixins are Dead, Long Live Composition Container Components Atomic Web Design Building the Facebook News Feed with Relay

  • 在本文的早期版本中,我把它们称为 "Smart"和 "Dumb"的组件,但这对展示组件来说过于苛刻,而且最重要的是,没有真正解释它们的目的的差异。我更喜欢这个新的术语,我希望你也是如此 ** 在本文的早期版本中,我声称展示性组件应该只包含其他展示性组件。我不再认为这是事实。一个组件是展示型组件还是容器属于它的实现细节。你应该能够在不修改任何调用站点的情况下,用容器替换一个展示型组件。因此,展示型组件和容器组件都可以包含其他展示型组件或容器组件就好了。