作者:kurtshen
译自react-js-presentational-container-components,by Krasimir Tsonev.
当我们开始使用 React 时,我们很快会开始遇到疑惑。在哪里放置数据,组件间变化如何通信或如何管理状态?问题的答案往往是与场景相关,也有时候只是跟平常使用 react 库来做的练习与实验有关。 然而,有一种广泛使用并有助于组织基于React的应用模式 —— 将组件拆分为展示(presentational)组件和(container)容器组件。
本文是 React 模式系列的一部分。检出这个仓库来了解在使用React开发应用时使用的更多技术。
让我们从一个简单的例子开始,说明问题,然后将组件拆分为容器和展示组件。 我们将使用一个 clock
组件。 它接受一个Date
对象作为prop
,并显示实时变化的时间。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = { time: this.props.time };
this._update = this._updateTime.bind(this);
}
render() {
var time = this._formatTime(this.state.time);
return (
<h1>{ time.hours } : { time.minutes } : { time.seconds }</h1>
);
}
componentDidMount() {
this._interval = setInterval(this._update, 1000);
}
componentWillUnmount() {
clearInterval(this._interval);
}
_formatTime(time) {
var [ hours, minutes, seconds ] = [
time.getHours(),
time.getMinutes(),
time.getSeconds()
].map(num => num < 10 ? '0' + num : num);
return { hours, minutes, seconds };
}
_updateTime() {
this.setState({ time: new Date(this.state.time.getTime() + 1000) });
}
};
ReactDOM.render(<Clock time={ new Date() }/>, ...);
在组件的构造函数中,我们将传递的time对象存储到内部状态。 通过使用`setInterval`,我们每秒更新状态,组件被重新渲染。 为了使它看起来像一个真正的时钟,我们使用两个辅助方法 —— `_formatTime`和`_updateTime`。`_formatTime`方法是提取小时,分钟和秒,并确保他们遵循两位数格式。`_updateTime`以一秒为度量来改变当前的`time`对象。
### 问题
在我们的组件这里有几件事情会发生。看起来这个组件有太多的职责。
- 它自己改变状态。 更改组件内部的时间可能不是一个好主意,因为只有`clock`知道当前的值。 如果系统的另一部分依赖于此数据,则很难共用它。
- `_formatTime`实际上是做两件事 —— 它从日期对象中提取所需的信息,并确保这些值始终为两位数。 这看起来没问题,但如果提取的方法不是这个组件的一部分,这将是很好的。因为Clock绑定了`time`对象的类型(作为一个prop)。 也就是说它需要知道关于数据形态的细节。
### 解决思路
那么,让我们将组件分为两个部分 - 容器和展示组件。
#### 容器
容器知道数据,知道数据的形态以及数据从何而来。 他们知道事务如何运作的细节或者说所谓的业务逻辑。 它们接收信息并对其进行格式化,以便由展示组件简单地使用。 通常我们使用[高阶组件(higher-order components)](https://github.com/krasimir/react-in-patterns/tree/master/patterns/higher-order-components)来创建容器。 它们的render方法仅包含展示组件。 在[flux架构(flux architecture)](https://github.com/krasimir/react-in-patterns/tree/master/patterns/flux)的上下文中,这是绑定了stores的变化和调用action的创建者的。
下面是我们的`ClockContainer`:
```javascript
// Clock/index.js
import Clock from './Clock.jsx'; // <-- 展示组件
export default class ClockContainer extends React.Component {
constructor(props) {
super(props);
this.state = { time: props.time };
this._update = this._updateTime.bind(this);
}
render() {
return <Clock { ...this._extract(this.state.time) }/>;
}
componentDidMount() {
this._interval = setInterval(this._update, 1000);
}
componentWillUnmount() {
clearInterval(this._interval);
}
_extract(time) {
return {
hours: time.getHours(),
minutes: time.getMinutes(),
seconds: time.getSeconds()
};
}
_updateTime() {
this.setState({ time: new Date(this.state.time.getTime() + 1000) });
}
};
它仍然接受time
(日期对象),执行setInterval
循环并了解有关数据(getHours
,getMinutes
和getSeconds
)的详细信息。 最终渲染到展示组件并传递小时,分钟和秒三个数字。
展示组件
展示组件是与展示的东西样子相关的。 他们有着让页面变得漂亮所需的额外的修饰。这样的组件不绑定任何东西,并且没有依赖性。 通常被实现为无状态功能组件(stateless functional components),也就是说它们没有内部状态。
在我们的例子中,展示组件只包含两位数字的检查并返回<h1>
标签:
// Clock/Clock.jsx
export default function Clock(props) {
var [ hours, minutes, seconds ] = [
props.hours,
props.minutes,
props.seconds
].map(num => num < 10 ? '0' + num : num);
return <h1>{ hours } : { minutes } : { seconds }</h1>;
};
好处
将组件拆分为容器和展示组件增加了组件的可重用性。 我们的Clock
函数/组件可能存在于不改变时间或不使用JavaScript的Date对象的应用程序中。 这是因为它是漂亮的_傀儡_。 没有关于数据的细节,只有它的初始形态和它来自哪里。
关于容器的好处是它们封装逻辑并且可以将数据注入到不同的渲染器中。 通常,导出容器的代码不直接导出一个类,而是一个函数。 例如,不是使用
import Clock from './Clock.jsx';
export default class ClockContainer extends React.Component {
render() {
return <Clock />;
}
}
而是我们可以导出一个接受展示组件的函数:
export default function(Component) {
return class Container extends React.Component {
render() {
return <Component />;
}
}
}
使用这种技术我们的容器是真正灵活的渲染其结果。 如果我们要从数字时钟的展示样式转换到模拟时钟的展示样式,这将是非常有用的。
因为我们对于我们的组件必须考虑更少,使得测试也会变得容易。 容器不关心UI东西,并且通常触发逻辑的动作由回调控制。展示组件只是呈现传入的props
,并且如果某处被点击/填充(数据),他们的单元测试或多或少地会检查正确的回调是否被调用。
其他资源
- Presentational and Container Components by Dan Abramov
- Container Components by “Learn React with chantastic”
旁注
没有什么是一成不变的。现实组件有时有内部状态。容器可能有额外增加的部分。这里描述的概念没有严格的规则,怎么去做取决于具体的场景。
相关阅读推荐