五、组合你的 React 组件

既然您知道了如何创建带状态和不带状态的 React 组件,我们就可以开始组合 React 组件并构建更复杂的用户界面了。事实上,现在是我们开始构建名为Snapterest的 web 应用的时候了,我们在第 2 章中讨论了为您的项目安装强大的工具。在此过程中,您将学习如何规划 React 应用并创建可组合的 React 组件。开始吧。

使用 React 解决问题

在开始为 web 应用编写代码之前,您需要考虑您的 web 应用将要解决的问题。了解尽可能清楚和尽早地定义问题是迈向成功解决方案(一个有用的 web 应用)最重要的一步,这一点非常重要。若您在开发过程的早期未能定义问题,或者定义不准确,那个么稍后您将不得不停止,重新思考您正在做的事情,扔掉您已经编写的一段代码,并编写一个新的代码。这是一种浪费的方法,作为一名专业软件开发人员,您的时间不仅对您非常宝贵,而且对您的组织也非常宝贵,因此明智地进行投资符合您的最佳利益。在本书前面,我强调了使用 React 的好处之一是代码重用,这意味着您将能够在更短的时间内完成更多的工作。然而,在我们看 React 代码之前,让我们先讨论一下这个问题,记住 React。

我们将构建 Snapterest—一个 web 应用,它以实时方式从 Snapkite 引擎服务器接收推文,并向用户一次显示一条推文。我们实际上不知道 Snapterest 何时会收到新的推文,但当它收到时,它会显示该新推文至少 1.5 秒,以便用户有足够的时间查看并单击它。点击一条 tweet 会将其添加到现有 tweet 集合或创建一条新 tweet。最后,用户将能够将其集合导出为 HTML 标记代码。

这是对我们将要构建的内容的一个非常高层次的描述。让我们将其分解为一系列较小的任务:

Solving a problem using React

以下是步骤:

  1. 从 Snapkite 引擎服务器实时接收推文。
  2. 一次显示一条 tweet,持续至少 1.5 秒。
  3. 在用户单击事件中将推文添加到集合中。
  4. 显示集合中的推文列表。
  5. 为集合创建 HTML 标记代码并将其导出。
  6. 在用户单击事件时从集合中删除推文。

您能否确定哪些任务可以使用 React 解决?请记住,React 是一个用户界面库,因此任何描述用户界面以及与该用户界面的交互的内容都可以通过 React 来解决。在前面的列表中,React 可以处理除第一个任务之外的所有任务,因为它描述的是数据获取,而不是用户界面。第 1 步将通过另一个库解决,我们将在下一章中讨论。步骤 2 和 4 描述了需要显示的内容。它们是 React 组件的完美候选者。步骤 3 和 6 描述了用户事件,正如我们在第 4 章中所看到的,创建您的第一个 React 组件,用户事件处理的处理也可以封装在 React 组件中。你能想到如何用 React 解决步骤 5 吗?还记得在第 3 章创建第一个 React 元素中,我们讨论了将 React 元素呈现为静态 HTML 标记字符串的ReactDOMServer.renderToStaticMarkup()方法。这正是我们解决第 5 步所需要的。

现在,当我们为每个单独的任务确定了一个潜在的解决方案后,让我们考虑如何将它们组合在一起,并创建一个功能齐全的 web 应用。

有两种方法可以构建可组合的 React 应用:

  • 首先,您可以从构建单个 React 组件开始,然后将它们组合到更高级别的 React 组件中,向组件层次结构的上游移动
  • 您可以从最顶层的 React 元素开始,然后实现其子组件,向下移动组件层次结构

第二种策略的优点是可以看到并理解应用架构的全局,我认为在我们考虑如何实现各个功能之前,了解所有内容是如何组合在一起的是很重要的。

规划您的 React 应用

在规划 React 应用时,我们需要遵循两个简单的指导原则:

  • 每个 React 组件都应该表示 web 应用中的单个用户界面元素。它应该封装可能被重用的最小元素。
  • 多个反应组分应组成一个反应组分。最终,您的整个用户界面应该封装在一个 React 组件中。

Planning your React application

React 组件层次结构图

我们将从我们最重要的反应成分应用开始。它将封装我们的整个 React 应用,它将有两个子组件:集合组件。组件将负责连接到推文流,接收并显示最新的推文。组件将有两个子组件:流推特StreamTweet组件将负责显示最新的 tweet。它将由报头Tweet组件组成。标题组件将呈现标题。它将没有子组件。Tweet组件将渲染来自 Tweet 的图像。请注意,我们计划如何重复使用组件两次。

采集组件将负责显示采集控件和 tweet 列表。它将有两个子组件:采集控件推文列表CollectionControls组件将有两个子组件:CollectionRenameForm组件将呈现表单以重命名集合,而CollectionExportForm组件将呈现表单以将集合导出到名为CodePen的服务,这是一个 HTML、CSS 和 JavaScript 的网站。您可以在了解更多关于 CodePen 的 http://codepen.io 。您可能已经注意到,我们将重用集合重命名表单集合控件组件中的标题按钮组件。我们的推文列表组件将呈现推文列表。每个 tweet 将由一个tweet组件呈现。我们将在我们的集合组件中再次重用组件。事实上,我们总共将重用组件五次。这是我们的胜利。正如我们在前一章中所讨论的,我们应该尽可能多地保持 React 组件无状态。因此,11 个组件中只有 5 个将存储状态,如下所示:

  • 申请
  • 采集控制
  • 集合重命名表单
  • 流推特

现在当我们有了一个计划,我们就可以开始实施了。

创建容器 React 组件

让我们从编辑应用的主 JavaScript 文件开始。将~/snapterest/source/app.js文件的内容替换为以下代码段:

import React from 'react';
import ReactDOM from 'react-dom';
import Application from './components/Application';

ReactDOM.render(
  <Application />,
  document.getElementById('react-application')
);

这个文件中只有四行代码,您可以猜到,它们提供了document.getElementById('react-application')作为<Application/>组件的部署目标,并将<Application/>呈现给 DOM。我们的 web 应用的整个用户界面将封装在一个 React 组件Application中。

接下来,导航到~/snapterest/source/components/并在此目录中创建Application.js文件:

import React, { Component } from 'react';
import Stream from './Stream';
import Collection from './Collection';

class Application extends Component {
  state = {
    collectionTweets: {}
  }

  addTweetToCollection = (tweet) => {
    const { collectionTweets } = this.state;

    collectionTweets[tweet.id] = tweet;

    this.setState({
      collectionTweets: collectionTweets
    });
  }

  removeTweetFromCollection = (tweet) => {
    const { collectionTweets } = this.state;

    delete collectionTweets[tweet.id];

    this.setState({
      collectionTweets: collectionTweets
    });
  }

  removeAllTweetsFromCollection = () => {
    this.setState({
      collectionTweets: {}
    });
  }

  render() {
    const {
      addTweetToCollection,
      removeTweetFromCollection,
      removeAllTweetsFromCollection
    } = this;

    return (
      <div className="container-fluid">
        <div className="row">
          <div className="col-md-4 text-center">
            <Stream onAddTweetToCollection={addTweetToCollection}/>
          </div>
          <div className="col-md-8">
            <Collection
              tweets={this.state.collectionTweets}
              onRemoveTweetFromCollection={removeTweetFromCollection}
              onRemoveAllTweetsFromCollection={removeAllTweetsFromCollection}
            />
          </div>
        </div>
      </div>
    );
  }
}

export default Application;

此组件的代码比我们的app.js文件多得多,但此代码可以很容易地分为三个逻辑部分:

  • 导入依赖模块
  • 定义 React 组件类
  • 将 React 组件类导出为模块

Application.js文件的第一个逻辑部分中,我们使用require()函数导入依赖模块:

import React, { Component } from 'react';
import Stream from './Stream';
import Collection from './Collection';

我们的Application组件将有两个子组件需要导入:

  • Stream组件将呈现我们用户界面的流部分
  • Collection组件将呈现我们用户界面的集合部分

我们还需要将React库作为另一个模块导入。

Application.js文件的第二个逻辑部分使用以下方法创建 ReactApplication组件类:

  • addTweetToCollection()
  • removeTweetFromCollection()
  • removeAllTweetsFromCollection()
  • render()

只有render()方法是 React API 的一部分。所有其他方法都是这个组件封装的应用逻辑的一部分。在讨论此组件在其render()方法中呈现的内容后,我们将仔细查看每一个组件:

render() {
  const {
    addTweetToCollection,
    removeTweetFromCollection,
    removeAllTweetsFromCollection
  } = this;

  return (
    <div className="container-fluid">
      <div className="row">
        <div className="col-md-4 text-center">
          <Stream onAddTweetToCollection={addTweetToCollection}/>
        </div>
        <div className="col-md-8">
          <Collection
            tweets={this.state.collectionTweets}
            onRemoveTweetFromCollection={removeTweetFromCollection}
            onRemoveAllTweetsFromCollection={removeAllTweetsFromCollection}
          />
        </div>
      </div>
    </div>
  );
}

如您所见,它使用引导框架定义了我们的网页布局。如果您不熟悉引导,我强烈建议您访问http://getbootstrap.com 并阅读文档。学习此框架将使您能够快速轻松地创建用户界面原型。即使您不知道引导,也很容易理解发生了什么。我们将网页分为两列:一列较小,一列较大。较小的包含我们的Stream反应成分,较大的包含我们的Collection成分。您可以想象,我们的网页被分成两个不相等的部分,它们都包含 React 组件。

这就是我们使用Stream组件的方式:

<Stream onAddTweetToCollection={addTweetToCollection} />

Stream组件有一个onAddTweetToCollection属性,我们的Application组件通过自己的addTweetToCollection()方法作为该属性的值。addTweetToCollection()方法将 tweet 添加到集合中。这是我们在Application组件中定义的自定义方法之一。我们不需要this关键字,因为该方法被定义为一个箭头函数,因此函数的范围自动成为我们的组件。

让我们来看看 HORT T0 方法是什么:

addTweetToCollection = (tweet) => {
  const { collectionTweets } = this.state;

  collectionTweets[tweet.id] = tweet;

  this.setState({
    collectionTweets: collectionTweets
  });
}

此方法引用存储在当前状态下的收集 tweet,向collectionTweets对象添加新 tweet,并通过调用setState()方法更新状态。当在Stream组件内调用addTweetToCollection()方法时,新的 tweet 将作为参数传递。这是一个子组件如何更新其父组件状态的示例。

这是 React 中的一个重要机制,其工作原理如下:

  1. 父组件将回调函数作为属性传递给其子组件。子组件可以通过this.props引用访问此回调函数。
  2. 每当子组件想要更新父组件的状态时,它都会调用该回调函数,并将所有必要的数据传递给新的父组件的状态。
  3. 父组件更新其状态,正如您已经知道的,该状态会更新并触发render()方法,该方法会根据需要重新呈现所有子组件。

这是子组件与父组件交互的方式。这种交互允许子组件将应用的状态管理委托给其父组件,而它只关心如何呈现自己。现在,当您了解了这个模式后,您将一次又一次地使用它,因为大多数 React 组件都应该保持无状态。只有少数父组件应该存储和管理应用的状态。此最佳实践使我们能够通过两个不同的关注点对 React 组件进行逻辑分组:

  • 管理应用的状态并呈现它
  • 仅呈现应用的状态管理并将其委托给父组件

我们的Application组件有第二个子组件Collection

<Collection
  tweets={this.state.collectionTweets}
  onRemoveTweetFromCollection={removeTweetFromCollection}
  onRemoveAllTweetsFromCollection={removeAllTweetsFromCollection}
/>

此组件具有多个属性:

  • tweets:这是指我们目前收集的推文
  • onRemoveTweetFromCollection:这是指从我们的收藏中删除特定推文的功能
  • onRemoveAllTweetsFromCollection:这是指从我们的收藏中删除所有推文的功能

您可以看到,Collection组件的属性只关心如何执行以下操作:

  • 访问应用的状态
  • 改变应用的状态

正如您所猜测的,onRemoveTweetFromCollectiononRemoveAllTweetsFromCollection函数允许Collection组件改变Application组件的状态。另一方面,tweets属性将Application组件的状态传播到Collection组件,以便它可以获得对该状态的只读访问。

您能识别ApplicationCollection组件之间的单向数据流吗?下面是它的工作原理:

  1. Application组件的constructor()方法中初始化collectionTweets数据。
  2. collectionTweets数据作为tweets属性传递给Collection组件。
  3. Collection组件调用removeTweetFromCollectionremoveAllTweetsFromCollection函数更新Application组件中的collectionTweets数据,循环再次开始。

注意,Collection组件不能直接改变Application组件的状态。Collection组件通过this.props对象只读访问该状态,更新父组件状态的唯一方法是调用父组件传递的回调函数。在Collection组件中,这些回调函数是this.props.onRemoveTweetFromCollectionthis.props.onRemoveAllTweetsFromCollection

这个关于 React 组件层次结构中数据如何流动的简单心智模型将帮助我们增加使用的组件数量,而不会增加用户界面工作的复杂性。例如,它最多可以有 10 级嵌套的 React 组件,如下所示:

Creating a container React component

如果Component G想要改变根Component A的状态,它将以与Component BComponent F完全相同的方式进行,或者与此层次结构中的任何其他组件完全相同。但是,在 React 中,您不应该将数据从Component A直接传递到Component G。相反,您应该先将其传递到Component B,然后传递到Component C,再传递到Component D,依此类推,直到您最终到达Component GComponent BComponent F必须携带一些实际上仅用于Component G的“运输”属性。这看起来可能是浪费时间,但这种设计使我们能够轻松地调试应用并分析其工作原理。始终存在优化应用体系结构的策略。其中之一是使用流量设计模式。另一个是使用Redux库。我们将在本书后面讨论这两个问题。

在讨论完我们的 Frutt0 组件之前,让我们看看两种方法来改变它的状态:

removeTweetFromCollection = (tweet) => {
  const { collectionTweets } = this.state;

  delete collectionTweets[tweet.id];

  this.setState({
     collectionTweets: collectionTweets
  });
}

removeTweetFromCollection()方法从我们存储在Application组件状态的 tweet 集合中删除 tweet。它从组件的状态中获取当前的collectionTweets对象,从该对象中删除具有给定id的 tweet,并使用更新的collectionTweets对象更新组件的状态。

另一方面,removeAllTweetsFromCollection()方法从组件状态中删除所有 tweet:

removeAllTweetsFromCollection = () => {
  this.setState({
    collectionTweets: {}
  });
}

这两种方法都是从孩子的Collection组件调用的,因为该组件没有其他方法来改变Application组件的状态。

总结

在本章中,您学习了如何使用 React 解决问题。我们首先将问题分解为更小的单个问题,然后讨论如何使用 React 解决这些问题。然后,我们创建了需要实现的 React 组件列表。最后,我们创建了第一个可组合的 React 组件,并了解了父组件如何与其子组件交互。

在下一章中,我们将实现子组件,并了解 React 的生命周期方法。