使用 React 提升水平:容器组件

Avatar of Brad Westfall
Brad Westfall

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

本教程是 Brad Westfall 关于 React 的三部分系列教程中的第二部分。本系列旨在帮助您超越基本的 React 技能,构建更大的项目,例如完整的单页应用程序 (SPA)。本文承接上一篇文章(关于 React Router)的内容。

文章系列

  1. React Router
  2. 容器组件 (您当前位置!)
  3. 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。其他类似的工具包括 gotfetchSuperAgent

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** 中的方法如何访问 foobar 作为 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 中的事件附加在视图级别。出于关注点分离的目的,如果我们在视图中创建事件函数,这可能会在我们的展示组件中造成问题。

为了详细说明,让我们首先将事件添加到展示组件(您可以点击的 `