在使用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)

  1. 关注页面的展示效果(外观)
  2. 内部可以包含展示组件和容器组件,通常会包含一些自己的DOM标记和样式(style)
  3. 通常允许通过this.props.children方式来包含其他组件。
  4. 对应用程序的其他部分没有依赖关系,例如Flux操作或store。
  5. 不用关心数据是怎么加载和变动的。
  6. 只能通过props的方式接收数据和进行回调(callback)操作。
  7. 很少拥有自己的状态,即使有也是用于展示UI状态的。
  8. 会被写成函数式组件除非该组件需要自己的状态,生命周期或者做一些性能优化。

Example:Page,Header,Sidebar,UserInfo,List

容器组件(Container Component)

  1. 关注应用的是如何工作的
  2. 内部可以包含容器组件和展示组件,但通常没有任何自己的DOM标记,除了一些包装divs,并且从不具有任何样式。
  3. 提供数据和行为给其他的展示组件或容器组件。
  4. 调用Flux操作并将它们作为回调函数提供给展示组件。
  5. 往往是有状态的,因为它们倾向于作为数据源
  6. 通常使用高阶组件生成,例如React Redux的connect(),Relay的createContainer()或Flux Utils的Container.create(),而不是手工编写。

Example:UserPage, FollowersSidebar, StoryContainer, FollowedUserList

优点(benifit)

  1. 展示和容器组件更好的分离,有助于更好的理解应用和UI
  2. 重用性高,展示组件可以用于多个不同数据源。
  3. 展示组件就是你的调色板,可以把他们放到单独的页面,在不影响应用程序的情况下,让设计师调整UI。
  4. 这迫使您提取诸如侧边栏,页面,上下文菜单等“布局组件”并使用this.props.children,而不是在多个容器组件中复制相同的标记和布局。

何时引入容器组件

我建议你从开始创建组件时只使用展示组件,到最后会意识到你传递了很多props到中间组件,而这些中间组件根本不会用到他们接收到的这些props,仅仅是向下传递。而当这些子组件需要更多的数据时,你需要从新配置这些中间组件。这个时候就需要引入容器组件了。使用容器组件的方式,您可以将数据和行为通过props传递给叶子组件,而不必麻烦一些不相关的中间组件。
这是一个重构的过程,所以不比在第一次就做对。当你尝试这种模式。在何时应抽取为容器组件你将会有一种直观的感觉。就像您知道何时抽取函数一样

二分法

重要的是你需要明白容器组件和展示组件之间不是技术上的区别,而是目的上的区别。

相比之下,这里有几个相关的(但不同的)技术区别:

  1. 有状态【Stateful】和 无状态【Stateless】:
    容器组件倾向于有状态,展示组件倾向于无状态,这不是硬性规定,因为容器组件和展示组件都可以是有状态的。

  2. 类【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的官网看一下函数组件和类组件

  1. 纯粹【Pure】 和 不纯粹 【Impure】:
    纯粹:输入什么就输出什么,不会再其中做相应的变动。可以被定义为类或函数,可以是无状态或有状态的,纯组件的另一个重要方面是它们不依赖props或state中的深度变动,所以他们的渲染性能可以通过在shouldComponentUpdate()钩子中进行浅层比较来优化,当前只有类可以定义shouldComponentUpdate()方法。

不管是展示组件还是容器组件都会有上面的二分特性。在我看来,展示组件往往是没有状态的纯函数,而容器组件往往是有状态的纯类,这仅仅个人观点并非规则。

当不重要或说很难分清时,不要把分离容器组件和展示组件当做教条,如果你不确定该组件是容器组件还是展示组件是,就暂时不要分离,写成展示组件吧。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐