在使用React中,你是否会出现过一个文件的代码很多,既存在应用数据的读取和处理,又存在数据的显示,而且每个组件还不能复用。
首先我们来看一个容器组件和展示组件一起的例子吧。
class TodoList extends React.Component{
constructor(props){
super(props);
this.state ={
todos:[]
}
this.fetchData = this.fetchData.bind(this);
}
componentDidMount(){
this.fetchData();
}
fetchData(){
fetch('/api/todos').then(data =>{
this.setState({
todos:data
})
})
}
render(){
const {todos} = this.state;
return (<div>
<ul>
{todos.map((item,index)=>{
return <li key={item.id}>{item.name}</li>
})}
</ul>
</div>)
}
}
大家可以看到这个例子是没有办法复用的,因为数据的请求和数据的展示都在一个组件进行,要实现组件的复用,我们就需要将展示组件和容器组件分离出来。
具体代码如下:
//展示组件
class TodoList extends React.Component{
constructor(props){
super(props);
}
render(){
const {todos} = this.props;
return (<div>
<ul>
{todos.map((item,index)=>{
return <li key={item.id}>{item.name}</li>
})}
</ul>
</div>)
}
//容器组件
class TodoListContainer extends React.Component{
constructor(props){
super(props);
this.state = {
todos:[]
}
this.fetchData = this.fetchData.bind(this);
}
componentDidMount(){
this.fetchData();
}
fetchData(){
fetch('/api/todos').then(data =>{
this.setState({
todos:data
})
})
}
render(){
return (<div>
<TodoList todos={this.state.todos} />
</div>)
}
}
当我们把组件分离成容器组件和展示组件这两类时,你会发现他们能够很方便的实现复用。
展示组件(Presentational Component)
- 关注页面的展示效果(外观)
- 内部可以包含展示组件和容器组件,通常会包含一些自己的DOM标记和样式(style)
- 通常允许通过this.props.children方式来包含其他组件。
- 对应用程序的其他部分没有依赖关系,例如Flux操作或store。
- 不用关心数据是怎么加载和变动的。
- 只能通过props的方式接收数据和进行回调(callback)操作。
- 很少拥有自己的状态,即使有也是用于展示UI状态的。
- 会被写成函数式组件除非该组件需要自己的状态,生命周期或者做一些性能优化。
Example:
Page,Header,Sidebar,UserInfo,List
容器组件(Container Component)
- 关注应用的是如何工作的
- 内部可以包含容器组件和展示组件,但通常没有任何自己的DOM标记,除了一些包装divs,并且从不具有任何样式。
- 提供数据和行为给其他的展示组件或容器组件。
- 调用Flux操作并将它们作为回调函数提供给展示组件。
- 往往是有状态的,因为它们倾向于作为数据源
- 通常使用高阶组件生成,例如React Redux的connect(),Relay的createContainer()或Flux Utils的Container.create(),而不是手工编写。
Example:
UserPage, FollowersSidebar, StoryContainer, FollowedUserList
优点(benifit)
- 展示和容器组件更好的分离,有助于更好的理解应用和UI
- 重用性高,展示组件可以用于多个不同数据源。
- 展示组件就是你的调色板,可以把他们放到单独的页面,在不影响应用程序的情况下,让设计师调整UI。
- 这迫使您提取诸如侧边栏,页面,上下文菜单等“布局组件”并使用this.props.children,而不是在多个容器组件中复制相同的标记和布局。
何时引入容器组件
我建议你从开始创建组件时只使用展示组件,到最后会意识到你传递了很多props到中间组件,而这些中间组件根本不会用到他们接收到的这些props,仅仅是向下传递。而当这些子组件需要更多的数据时,你需要从新配置这些中间组件。这个时候就需要引入容器组件了。使用容器组件的方式,您可以将数据和行为通过props传递给叶子组件,而不必麻烦一些不相关的中间组件。
这是一个重构的过程,所以不必在第一次就做对。当你尝试这种模式。在何时应抽取为容器组件你将会有一种直观的感觉。就像您知道何时抽取函数一样
二分法
重要的是你需要明白容器组件和展示组件之间不是技术上的区别,而是目的上的区别。
相比之下,这里有几个相关的(但不同的)技术区别:
- 有状态【Stateful】和 无状态【Stateless】:
容器组件倾向于有状态,展示组件倾向于无状态,这不是硬性规定,因为容器组件和展示组件都可以是有状态的。
- 类【Classes】 和 函数【Functions】:
组件可以被申明成类或函数,函数组件定义简单,但是他缺乏目前仅用于类的一些功能。虽然函数组件有很多限制,但是直到现在还有人使用,是因为函数组件容易理解,建议在不需要自己的state,lifecycle hooks,或性能优化的情况下使用函数组件。这些仅适用于类组件。
//我们将上边的展示组件改写成函数组件可以如下
function TodoList(props){
return (<div>
<ul>
{props.todos.map((item,index)=>{
return <li key={item.id}>{item.name}</li>
})}
</ul>
</div>)
}
可能很多人不清楚函数组件和类组件的区别,可以去React的官网看一下函数组件和类组件
- 纯粹【Pure】 和 不纯粹 【Impure】:
纯粹:输入什么就输出什么,不会再其中做相应的变动。可以被定义为类或函数,可以是无状态或有状态的,纯组件的另一个重要方面是它们不依赖props或state中的深度变动,所以他们的渲染性能可以通过在shouldComponentUpdate()钩子中进行浅层比较来优化,当前只有类可以定义shouldComponentUpdate()方法。
不管是展示组件还是容器组件都会有上面的二分特性。在我看来,展示组件往往是没有状态的纯函数,而容器组件往往是有状态的纯类,这仅仅个人观点并非规则。
当不重要或说很难分清时,不要把分离容器组件和展示组件当做教条,
如果你不确定该组件是容器组件还是展示组件是,就暂时不要分离,写成展示组件吧。
译文来源:https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0