本教程是 Brad Westfall 关于 React 的三部分系列教程中的第二部分。本系列旨在帮助您超越基本的 React 技能,构建更大的项目,例如完整的单页应用程序 (SPA)。本文承接上一篇文章(关于 React Router)的内容。
文章系列
- React Router
- 容器组件 (您当前位置!)
- Redux
在 第一篇文章 中,我们创建了路由和视图。在本教程中,我们将探索一个新的概念,其中组件不创建视图,而是促进视图的创建。如果您想直接深入代码,可以在 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 请求也没有错(尽管我们可能应该将实际的调用抽象到其他函数中)。问题在于我们同时在存储视图的同一组件中执行这些操作。这种紧耦合使应用程序更僵化且 WET。如果您也需要在其他地方获取用户列表怎么办?获取用户的操作与该视图绑定,因此不可重用。
第二个问题是我们正在使用 jQuery 进行 Ajax 调用。当然,jQuery 具有许多不错的功能,但其中大多数功能都与 DOM 渲染有关,而 React 有自己的方法来执行此操作。至于 jQuery 的非 DOM 功能(如 Ajax),您很可能可以找到许多更专注于单一功能的替代方案。
其中一个替代方案是 Axios,这是一种基于 Promise 的 Ajax 工具,与 jQuery 的基于 Promise 的 Ajax 功能非常相似(在 API 方面)。它们有多相似呢?
// jQuery
$.get('/path/to/user-api').then(function(response) { ... });
// Axios
axios.get('/path/to/user-api').then(function(response) { ... });
在接下来的示例中,我们将继续使用 Axios。其他类似的工具包括 got、fetch 和 SuperAgent。
Props 和 State
在深入了解容器组件和表现组件之前,我们需要澄清一下关于 props 和 state 的一些内容。
Props 和 state 在某种程度上是相关的,因为它们都为 React 组件“建模”数据。两者都可以从父组件传递到子组件。但是,父组件的 props 和 state 将仅成为其子组件的 props。
例如,假设**ComponentA**将其一些 props 和 state 传递给其子组件**ComponentB**。**ComponentA** 的 render
方法可能如下所示
// ComponentA
render: function() {
return <ComponentB foo={this.state.foo} bar={this.props.bar} />
}
即使 foo
在父组件中是“state”,它也将在子组件**ComponentB** 中成为“prop”。bar
的属性也成为子组件中的 prop,因为从父组件传递到子组件的所有数据都将在子组件中成为 props。此示例显示了**ComponentB** 中的方法如何访问 foo
和 bar
作为 props
// ComponentB
componentDidMount: function() {
console.log(this.props.foo);
console.log(this.props.bar);
}
在使用 Ajax 获取数据示例中,从 Ajax 接收到的数据被设置为组件的 state。该示例没有子组件,但您可以想象,如果有子组件,state 将“流动”作为 props 从父组件传递到子组件。
要更好地理解 state,请参阅 React 文档。从现在开始,本教程将把随时间变化的数据称为“state”。
是时候分手了
在使用 Ajax 获取数据示例中,我们创建了一个问题。我们的 UserList
组件可以工作,但它试图做太多事情。为了解决这个问题,让我们将 UserList
分解成两个分别承担不同角色的组件。这两个组件类型在概念上将被称为**容器组件**和**表现组件**,也称为“智能”和“哑”组件。
简而言之,容器组件获取数据并处理 state。然后将 state 作为 props 传递给表现组件,并呈现为视图。
术语“智能”与“哑”组件在社区中正在逐渐消失。我在这里只提及它们,以防您在较旧的文章中读到它们,这样您就会知道它们与容器与表现组件的概念相同。
表现组件
您可能不知道,但您在本教程系列中之前已经见过表现组件了。想象一下,在 UserList
组件管理自身 state 之前它是什么样子
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 接收用户数据。
表现组件是“哑”的,因为它们不知道它们接收到的 props 是如何产生的。它们不知道 state。
表现组件绝不应该更改 prop 数据本身。事实上,任何接收 props 的组件都应该认为该数据是不可变的,并且由父组件拥有。虽然表现组件不应更改 prop 中数据的含义,但它可以为视图格式化数据(例如,将 Unix 时间戳转换为更易于人类阅读的内容)。
在 React 中,事件通过 onClick
等属性直接附加到视图。但是,有人可能会想知道事件是如何工作的,因为表现组件不应该更改 props。为此,我们在下面专门设置了一个关于事件的部分。
迭代
在循环中创建 DOM 节点时,需要 key
属性 为唯一值(相对于其同级元素)。请注意,这仅适用于最高级别的 DOM 节点——在本例中为 <li>
。
此外,如果嵌套的 return
对您来说看起来很奇怪,可以考虑另一种方法,该方法通过将列表项的创建拆分为其自身函数来完成相同的事情
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>
);
}
});
容器组件
容器组件几乎总是表现组件的父组件。在某种程度上,它们充当表现组件和应用程序其余部分之间的中介。它们也被称为“智能”组件,因为它们了解整个应用程序。
由于容器组件和表现组件需要具有不同的名称,因此我们将此组件称为 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;
为简洁起见,这些示例省略了 require()
和 module.exports
语句。但在这种情况下,重要的是要表明容器组件将其相应的表现组件作为直接依赖项引入。为完整起见,此示例显示了所有必要的 require 语句。
容器组件的创建方式与其他 React 组件相同。它们也拥有与其他组件一样的 `render` 方法,只是它们自身不创建任何渲染内容。相反,它们返回展示组件 (Presentational Component) 的渲染结果。
**关于 ES6 箭头函数的简短说明:** 您可能会注意到上面示例中所需的经典 `var _this = this` 技巧。除了语法更简洁外,ES6 箭头函数还具有其他优势,从而避免了使用此技巧的必要性。为了让您专注于学习 React,本教程避免使用 ES6 语法,而是采用旧的 ES5 语法。但是,本系列的 GitHub 指南大量使用了 ES6,并且其 README 文件中有一些解释。
事件
到目前为止,我们已经展示了如何将状态从容器组件传递到展示组件,但行为呢?事件属于行为的范畴,它们通常需要修改数据。React 中的事件附加在视图级别。出于关注点分离的目的,如果我们在视图中创建事件函数,这可能会在我们的展示组件中造成问题。
为了详细说明,让我们首先将事件添加到展示组件(您可以点击的 `
// 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 :(
}
});
从技术上讲,这可以工作,但这不是一个好主意。很有可能,事件需要更改数据,而更改的数据应存储为状态——展示组件不应该知道的状态。
在我们的示例中,状态更改将是用户的“活跃度”,但您可以创建任何您想要与 `onClick` 绑定的函数。
一个更好的解决方案是像这样将功能从容器组件作为 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` 属性需要位于视图所在的位置——展示组件。但是,它调用的函数已移动到父容器组件。这样做更好,因为容器组件处理状态。
如果父函数碰巧更改了状态,则状态更改将导致父函数重新渲染,进而更新子组件。这在 React 中会自动发生。
以下是一个演示,它展示了容器组件上的事件如何更改状态,这将自动更新展示组件。
查看 Brad Westfall 在 CodePen 上的笔 React 容器组件演示。(@bradwestfall)
请注意此示例如何处理不可变数据并使用 .bind() 方法。
将容器组件与路由器一起使用
路由器不应该再直接使用 `UserList`。相反,它将直接使用 `UserListContainer`,后者又将使用 `UserList`。最终,`UserListContainer` 返回 `UserList` 的结果,因此路由器仍然会收到它需要的内容。
数据流和扩展运算符
在 React 中,将 props 从父组件传递到子组件的概念称为流。到目前为止的示例仅显示了简单的父子关系,但在实际应用中可能存在许多嵌套组件。想象一下,数据通过状态和 props 从高级父组件向下流经许多子组件。这是 React 中的一个基本概念,在我们继续学习下一个关于 Redux 的教程时,务必牢记这一点。
ES6 有一个新的扩展运算符,非常有用。React 已采用类似的 JSX 语法。这确实有助于 React 通过 props 传递数据的方式。本教程的 GitHub 指南也使用了它,因此请务必阅读有关此功能的指南文档。
无状态函数组件
从 React 0.14(于 2015 年底发布)开始,出现了一项新功能,可以更轻松地创建无状态(展示)组件。新功能称为无状态函数组件。
到目前为止,您可能已经注意到,随着您分离容器组件和展示组件,许多展示组件仅具有 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` 来访问 props。
这是一个关于无状态函数组件的非常好的Egghead.io 视频。
MVC
如您可能已经看到,React 不像传统的 MVC。通常将 React 称为“仅视图层”。我对这句话的问题在于,React 初学者很容易认为 React *应该*适合他们对传统 MVC 的熟悉程度,就好像它应该与来自第三方的传统控制器和模型一起使用一样。
虽然 React *确实*没有“传统控制器”,但它确实提供了以其自身特殊方式分离视图和行为的方法。我相信容器组件与传统 MVC 中的控制器具有相同的基本作用。
至于模型,我见过人们将 Backbone 模型与 React 一起使用,我相信他们对这是否对他们有效有各种各样的看法。但我并不认为传统模型是 React 的正确选择。
React 希望以一种不适合传统模型工作方式的方式传递数据。Flux 设计模式(由 Facebook 创建)是一种拥抱 React 内在传递数据能力的方法。在下一个教程中,我们将介绍 Redux,这是一个非常流行的 Flux 实现,也是我认为传统模型的替代方案。
总结
容器组件更多的是一个概念,而不是一个确切的解决方案。本教程中的示例只是一种实现方法。但是,这个概念被广泛接受,甚至Facebook 的政策是在其团队内部使用它们——尽管他们可能使用不同的术语。
本教程深受其他文章关于此主题的影响。请务必查看本教程的官方GitHub 指南以获取更多信息和容器组件的工作示例。
文章系列
- React Router
- 容器组件 (您当前位置!)
- Redux
很棒的主题和很棒的文章——期待第 3 部分!
在 React 中,是否有可能避免这种难看的状态代码?
根据 React 文档,`setState()` 执行浅合并。这意味着即使 `user` 对象本身的引用被复制到新的 `state` 对象中,它本身也应该保持不变。那么,为什么不这样做呢?
根据我的理解,以及在您的 CodePen 中进行的一些快速尝试,这应该可以正常工作,因为它本质上与大量代码相同的逻辑。
您怎么看?
感谢 Agop,这是一个很好的问题。所以您实际上是在直接修改 `this.state`,因为 `user` 是 `this.state` 的引用。请参阅这些文档 https://facebook.github.io/react/docs/component-api.html。阅读警告部分,其中说明“切勿直接修改 `this.state`”。
我知道用你的方法看起来更简单,但在 React 中,状态变异是一个大问题,我们直到下一篇文章才会深入探讨。我本可以使用 Lodash 或 Immutable.js 等更多工具使我的代码看起来更漂亮,并且仍然遵循变异最佳实践。但我试图避免在示例中引入更多工具。这是我想到的用原生 JS 实现的最佳方法(尽管,
Object.assign()
是 ES6 的)。Brad,使用
Object.assign()
并不能阻止修改状态。当你调用Object.assign()
时,你最终得到的是this.state
的浅拷贝,它仍然引用完全相同的用户列表(以及完全相同的用户对象)。你可以用这段代码确认这一点
在控制台中,你会看到
所以,你仍然在修改
this.state
中存在的完全相同的用户对象,只是通过一个新的引用来修改它。你可以在切换active
的代码之后添加这段代码来进一步确认这里有一个快速演示
http://codepen.io/agop/pen/ONmJRK
长话短说,所有这些代码都归结为我之前写的内容
如果你真的想避免修改状态本身,包括其任何嵌套对象,你需要执行
this.state
的深拷贝。一旦我们开始讨论深拷贝,我们就需要开始讨论性能。如果我们有 100 个用户怎么办?我们要创建所有 100 个用户的深拷贝,包括它们的所有属性,仅仅为了更新 1 个用户的 1 个属性吗?所以我和一些 JS 朋友在 Slack 上进行了长时间的讨论,以下是大家的共识。首先,如果我们遵循最佳实践,我们永远不应该修改
this.state
,请参阅我之前关于 React 文档的评论。其次,你关于我如何使用Object.assign()
以及其浅拷贝的观点是正确的。我应该这样做并只复制用户,而不是整个状态。现在
user === newUser
的测试结果为假。现在的问题是,因为状态是一个数组,所以很难将newUser
“切片”到其原始位置。有各种各样的技巧,但请记住,我试图避免使用 Lodash 等新工具或任何与笔无关的复杂性。不过,如果坚持不使用新工具和 ES6,一种方法是在这个替代的 CodePen 中 http://codepen.io/bradwestfall/pen/NNjWLR。同样,这仅仅是一种方法。不过,为了初学者着想,我真正想要的是一个可以演示从父组件到子组件传递函数的 CodePen。我并不想陷入这个不变性问题:) 所以我更新了原始的 CodePen,使其以非常简单易变的方式实现——并附带免责声明,说明它并非技术上的最佳实践。虽然你的方法代码更少,但我发现它对初学者来说容易让人困惑,并且偏离主题,因为再次强调,笔的目的在于教授其他内容,而要正确地使用不变性,我们需要涉及很多其他内容,这些内容并非笔的目的。
此外,如果担心性能问题,Immutable.js 和 React 自身的 Immutability Helpers 应该可以很好地满足你的需求 http://facebook.github.io/react/docs/update.html
Brad,我实际上正要写很多相同的东西,在进一步研究 React 最佳实践之后,但你抢先一步了。所以,没错,我们最初的代码(在对底层状态的实际操作方面是相同的)都不符合最佳实践。
如你所知,并且在你的评论中准确地描述了,你更新的版本也不符合最佳实践(新的用户对象,但相同的用户列表)。我会做一些小的调整,使代码对文章读者保持简单,同时完全遵循 React 不变性最佳实践
笔
http://codepen.io/agop/pen/grWbqx
我们没有修改
this.state
或其任何嵌套属性,但我们使代码保持简洁(没有使用 ES6,保持简单)。这样,你就不必深入 React 和 Immutability 的深处,也不必展示一个误导性的示例(老实说,谁会阅读评论并真正理解这个示例“几乎”是不变的?)。这也回答了我自己关于深拷贝的问题——我们不需要深拷贝。我们可以引用现有的对象,我们只需要创建实际发生变化的部分的浅拷贝。正如你在评论中提到的,所有这些都可以通过 React 自身的 Immutability Helpers 以及 Immutable-js 等工具来简化。
感谢 Agop,我认为如果有人阅读了所有这些内容,他们会说这篇文章很容易理解,但这个不变性的东西到底是怎么回事?——至少那些没有接触过它的人:) 我认为我们的对话已经足够详细地记录了这篇文章。CodePen 的评论实际上也包含一个指向此对话的链接。我认为我会保持现状,因为笔的主要目的是其他内容。感谢你提供的所有反馈和建议代码。
Brad,这是你的代码,你的文章,在我们俩之间,我们显然都在同一页上;)
尽管如此,我还是不明白在文章中保留这段代码的意义
这实际上是无效代码。它根本不起作用(在赋值之前,
users[index] === user
始终成立)。它只会让读者感到困惑,而你本人也反对让读者感到困惑(例如,“为什么他要将用户分配到同一个列表的相同索引处?”,“为什么他要将同一个列表传回状态?”等等)。我的代码,从头到尾,包括注释,只有 16 行,仅限 ES5,并且非常容易理解,即使不了解 React 中不变性的“原因”。你的代码,从头到尾,包括注释,有 17 行。
你说你不想在这篇文章中涉及不变性,这很好。然而,你的代码包含一个 5 行的注释块,讨论了它如何不遵循不变性最佳实践,并链接到一个对于 React 新手来说毫无意义的大讨论。我的代码根本没有提到不变性,它只是遵循了不变性。
长话短说,我只是想了解在文章中保留不符合最佳实践的代码背后的思考过程,而这篇文章在 Google 搜索“react container components”时已经排在第五位了。我认为如果 React 新手看到这篇文章,看到示例,快速跳过大段的注释(拜托,谁会阅读 CodePen 中的大段注释?),然后在示例下方阅读,这会让人感到沮丧
抱歉,我到此结束我的抱怨。
感谢 Agop,我稍后会看看:) 我这周正忙于撰写 Redux 文章以及一些工作上的事情
很棒的主题和很棒的文章!
期待第 3 部分 :)
喜欢这个系列,Brad。我读过你提到的两篇文章作为灵感,但直到阅读这篇文章,我才真正理解容器与展示组件的概念。谢谢!
谢谢 Kyle!
非常感谢 Brad!在过去的几年里,CSS-Tricks 帮助我提升了自己的技能。你的教程很容易理解,我真的很喜欢。期待第 3 部分! :)
此致,
Loraine Ortiz
谢谢 Loraine
这篇博文在 The React Newsletter #23 中被推荐。 http://us4.campaign-archive2.com/?u=29c888baee9c05ccb614e1e92&id=4d59132d66
你好,
React 组件的非常好的入门介绍!
我使用略微不同的技术详细阐述了这篇文章,您能否看看我的笔:http://codepen.io/bluurn/pen/xVJNWw
我只是想知道我是否正确(或不正确:))。
非常感谢!
总的来说,它看起来不错。不过我注意到链接不起作用。一个原因可能是你对花括号加了引号
<Link to="{'/users/' + user.id}">{user.name}</Link>
。如果一个属性要从 JS 中获取动态部分,那么你就不要对它加引号。另外,我希望你也阅读了本系列的第三部分,因为你会看到 Redux 中的状态管理与多组件应用程序的不同之处——感谢 bluurn