该篇为翻译文章:原文为:Leveling Up With React: Container Components

该系列文章目录:
第一部分:学习React Router-译文(原文)
第二部分:React的“容器组件”和“可视化组件”(当前)
第三部分:React 之 Redux-译文(原文)

译者:
“容器组件”和“可视化组件”在原文当中分别叫做Container ComponentsPresentational Components。另外,还有一种叫法是Smart ComponentsDumb Components

《学习React Router》一文中,我们创建了路由和界面。而这篇文章,我们来介绍一下另一种组件,这种组件不是用来创建可视化的界面的。同样为了大家方便学习,大家可以到GitHub获取代码。

在我们创建的应用当中,当然需要获取一些数据。如果你比较熟悉一些MVC模型的组件化开发,那么你可能会知道一般来说在前端将视图和数据结合起来并不是有什么正统的方法。当视图需要渲染数据出来的话,那么它需要知道数据是从哪里来的,数据是如何变化的,以及如何创建数据。

使用Ajax获取数据

来举一个不好的例子,让我们来获取一些数据到UserList组件当中:


// This is an example of tightly coupled view and data which we do not recommend var UserList = React.createClass({ getInitialState: function() { return { users: [] } }, componentDidMount: function() { var _this = this; $.get('/path/to/user-api').then(function(response) { _this.setState({users: response}) }); }, render: function() { return ( <ul className="user-list"> {this.state.users.map(function(user) { return ( <li key={user.id}> <Link to="{'/users/' + user.id}">{user.name}</Link> </li> ); })} </ul> ); } });

为什么上面写的东西看上去不太理想呢?。那是因为我们并没有把数据视图分开。

我们在组件状态当中使用getInitialState来初始化组件状态,这没错。我们在componentDidMount方法当中使用Ajax来获取数据,这也没错(虽然我们可以把这个操作放到外面的一个函数去)。问题在于,我们不该把这些放到一个可视化的组件当中来。这种紧耦合会让我们的代码变的非常的冗余。那么如果不在这里获取数据的话,应该在哪里做这个操作呢?获取数据这个操作只会在这个视图当中用到,所以,它不是一个需要重用的操作。

另外,还有一个问题,上面的代码当中,我们使用的jQuery,仅仅是为了使用里面的Ajax方法。其实这是完全不必要的,因为我们只是做一个Ajax的加载,jQuery里面许多其它的东西我们都是不需要的。所以我们可以用另外一个专门发送Ajax请求的插件来替代jQuery,比如Axios,其实它们用起来都是差不多的:


// jQuery $.get('/path/to/user-api').then(function(response) { ... }); // Axios axios.get('/path/to/user-api').then(function(response) { ... });

Props 和 State

在我们介绍容器组件可视化组件的区别时,我们需要了解一下props和state.

Props和state在某种意义上来说是有点关系的,它们都是React组件当中的model(数据)。它们都可以从父组件传到子组件。然而,父层的props和state到了子层组件只是它的props。
比如,ComponentA把它的props和state传到了子组件ComponentB。渲染ComponentA时应该是这样:


// ComponentA render: function() { return <ComponentB foo={this.state.foo} bar={this.props.bar} /> }

虽然fooComponentAstate,但它到了ComponentB当中则会变成propsComponentAbar传到子层也会变成ComponentBprops,这时,可以在ComponentB当中来操作foobar


// ComponentB componentDidMount: function() { console.log(this.props.foo); console.log(this.props.bar); }

前面,我们使用Ajax获取了数据,然后将获取到的数据设置成了组件的state当中,那个例子里面没有子组件,但是如果有呢?那么state就会变成子组件的props。

容器组件和可视化组件

我们在使用Ajax获取数据的时候,遗留了一个问题,UserList组件做了太多的事。为了解决这个问题,我们需要把它拆成两个组件来完成不同的工作。那就是把它们拆成容器组件可视化组件
简单点来说,容器组件是用来提供数据并且管理状态。然后再把状态传到可视化组件的props当中去渲染。

可视化组件

可视化组件就是我们一直写的用来渲染到页面上可以被看的见的组件:


var UserList = React.createClass({ render: function() { return ( <ul className="user-list"> {this.props.users.map(function(user) { return ( <li key={user.id}> <Link to="{'/users/' + user.id}">{user.name}</Link> </li> ); })} </ul> ); } });

可视化组件是一个“哑巴”,因为它不知道获取到的数据是什么样的,不知道props和state的变化。它永远不应该改变props中的数据。实际上,任何的组件从父级得到的props都是不可变化的。它只是负责把获取到的数据渲染出来。

循环

当我们在组件当中有了循环,key关键字是必须的,并且是惟一的。注意,key要加在被循环标签的最顶层,比如上面写的<li>

你也可以把循环项单独写到一个方法中去:


var UserList = React.createClass({ render: function() { return ( <ul className="user-list"> {this.props.users.map(this.createListItem)} </ul> ); }, createListItem: function(user) { return ( <li key={user.id}> <Link to="{'/users/' + user.id}">{user.name}</Link> </li> ); } });

容器组件

“容器组件”总是作为可视化组件的父级组件出现。某种程序上来说,它在可视组件之间是一个中间人,起到一个支架作用。“容器组件”又被称做“智能组件”。

在例子中,为了避免大家搞混淆,把容器组件的名字后面加一个Container,叫做UserListContainer


var React = require('react'); var axios = require('axios'); var UserList = require('../views/list-user'); var UserListContainer = React.createClass({ getInitialState: function() { return { users: [] } }, componentDidMount: function() { var _this = this; axios.get('/path/to/user-api').then(function(response) { _this.setState({users: response.data}) }); }, render: function() { return (<UserList users={this.state.users} />); } }); module.exports = UserListContainer;

容器组件的创建方法和可视化组件的创建方法是一样的,它们都有render方法。但是它渲染的已经创建好的可视组件。

事件

到目前为止,我们已经知道了如何把容器组件的state传到可视化组件当中,但是行为如何传过去呢?事件就是一种行为,事件通常是需要来操作数据的。在React当中,事件是在视图层进行绑定的。这和我们的想法相违背,因为我们不想在视图层去操作数据。
为了更清楚的说明这个问题,我们来给可视化组件添加一个事件(给<button>添加一个点击事件):


// Presentational Component var UserList = React.createClass({ render: function() { return ( <ul className="user-list"> {this.props.users.map(function(user) { return ( <li key={user.id}> <Link to="{'/users/' + user.id}">{user.name}</Link> <button onClick={this.toggleActive}>Toggle Active</button> </li> ); })} </ul> ); }, toggleActive: function() { // We shouldn't be changing state in presentational components :( } });

从技术角度来说,它是可以工作的,但是这不是一个好的解决方法。因为在事件方法中可能会有改变数据的操作,而这不应该是可视化组件该做的事。
在这个例子当中,state的改变可能是用户的激活状态,但是你可以给点击事件绑定任何方法。

其实,还有一种更好的解决方案,那就是从容器组件时传一个方法到可视化组件当中作为可视化组件的prop:


// Container Component var UserListContainer = React.createClass({ ... render: function() { return (<UserList users={this.state.users} toggleActive={this.toggleActive} />); }, toggleActive: function() { // We should change state in container components :) } }); // Presentational Component var UserList = React.createClass({ render: function() { return ( <ul className="user-list"> {this.props.users.map(function(user) { return ( <li key={user.id}> <Link to="{'/users/' + user.id}">{user.name}</Link> <button onClick={this.props.toggleActive}>Toggle Active</button> </li> ); })} </ul> ); } });

onClick是必须要写在可视化组件中了,但是事件函数写到了容器组件当中。
如果父级的组件有改变state的动作,那么就会影响到子组件。这在React当中是自己完成的。下面看一下Demo:

See the Pen React Container Component Demo by Brad Westfall (@bradwestfall) on CodePen.

在Router当中使用容器组件

现在,知道了容器组件,那么路由当中不应该再指定像UserList这样的组件了,我们应该用UserListContainerUserListContainer最终会返回UserList渲染出来的东西。

数据流和展开运算符

在React当中,父层的props传到子层叫做“流”。我们举的例子当中只是演示了简单的一级父子关系,但是在真正的应用当中,可能会有很多层的组件嵌套。这个概念一定要记住,因为我们将来说Redux的时候将会非常有用。

ES6当中的展开运算符非常的强大。React在JSX当中采用了相同的语法。这样对于数据的流动非常的有帮助。

无状态的组件

React 0.14版本开始,有一种更方便的方法来创建无状态(可视化)组件,就叫做 无状态组件

到目前为止,你可能注意到了,随着我们把插件分成了两派后,大部分的可视化组件当中只有一个render方法。针对这种情况,React提供了一种写法来写组件,就是可以写一个单独的方法:


// The older, more verbose way var Component = React.createClass({ render: function() { return ( <div>{this.props.foo}</div> ); } }); // The newer "Stateless Functional Component" way var Component = function(props) { return ( <div>{props.foo}</div> ); };

可以看到,这种写法非常的简洁。但是记住,这种方法只适合在只需要一个render方法的组件中适用。
另外,这种写法会自动把props传过来,这就意味着方法当中不需要使用this了。

MVC

你可能早该知道了,React并不是传统的MVC结构,它只是代表了V。也就是说如果需要组成一个MVC的应用,还需要第三方的插件来充当C和M。但在React的生态当中,没有传统的C的概念,因为我们上面有自己的方法把视图和行为分开了,我觉得容器组件就相当于传统MVC当中的C。
关于M这一层,我见过有人将 Backbone的models和React混在一起用。但我认为传统MVC里的M在React当中也是不能一一对应的。

React处理数据的方法是让数据“流动”,Facebook的Flux设计模式就是用来应对这种“流动”数据的。我们将来会说到Redux,它和Flux设计模式是做一样事情的东西。