四、创建第一个 React 组件

在上一章中,您学习了如何创建 React 元素以及如何使用它们来呈现 HTML 标记。您看到了使用 JSX 生成 React 元素是多么容易。在这一点上,您已经对 React 有了足够的了解,可以创建静态网页,我们在第 3 章创建您的第一个 React 元素中讨论了这一点。然而,我打赌这不是你决定学习反应的原因。您不希望只构建由静态 HTML 元素组成的网站。您希望构建对用户和服务器事件做出反应的交互式用户界面。对事件作出反应意味着什么?静态 HTML 元素如何反应?元素如何反应?在本章中,我们将在介绍 React 组件的同时回答这些问题和许多其他问题。

无状态与有状态

反应意味着从一种状态切换到另一种状态。这意味着你首先需要有一个状态,并且有能力改变这个状态。我们是否在 React 元素中提到了一种状态或改变这种状态的能力?不,他们是无国籍的。它们的唯一目的是构造和呈现虚拟 DOM 元素。事实上,我们希望它们以完全相同的方式渲染,因为我们为它们提供了完全相同的参数集。我们希望它们是一致的,因为这使我们很容易对它们进行推理。这是使用 React 的一个关键好处,它可以简化对 web 应用工作方式的推理。

如何向无状态元素添加状态?如果我们不能将状态封装在 React 元素中,那么我们应该将 React 元素封装在已经具有状态的东西中。想象一个代表用户界面的简单状态机。每个用户操作都会触发状态机中的状态变化。每个状态都由不同的 React 元素表示。在 React 中,此状态机称为React 组件

创建第一个无状态 React 组件

在下面的例子中,我们来看看如何创建一个 React 组件:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class ReactClass extends Component {
  render () {
    return (
      <h1 className="header">React Component</h1>
    );
  }
}

const reactComponent = ReactDOM.render(
  <ReactClass/>,
  document.getElementById('react-application')
);
export default ReactClass;

前面的一些代码对您来说应该已经很熟悉了,其余的代码可以分解为两个简单的步骤:

  1. 创建 React 组件类。
  2. 创建一个 React 组件。

让我们仔细看看我们如何创建一个 React 组件:

  1. 创建一个ReactClass类作为Component类的子类。在本章中,我们将重点学习如何更详细地创建 React 组件类。
  2. 通过调用ReactDOM.render()函数并提供我们的ReactClass元素作为其元素参数来创建reactComponent

我强烈建议您阅读 Dan Abramov 的这篇博文,其中更详细地解释了 React 组件、元素和实例之间的差异:https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html

我们的 React 组件的外观在ReactClass中声明。

Component类封装组件的状态并描述如何呈现组件。React 组件类至少需要有一个render()方法,以便返回nullfalse。以下是最简单形式的render()方法示例:

class ReactClass extends Component {
  render() {
    return null;
  }
}

正如您所猜测的,render()方法负责告诉 React 这个组件应该呈现什么。它可以返回null,如上例所示,屏幕上不会呈现。或者,它可以返回我们在第 3 章创建第一个 React 元素中学习的 JSX 元素:

class ReactClass extends Component {
  render() {
    return (
      <h1 className="header">React Component</h1>
    );
  }
}

这个例子展示了如何将 React 元素封装在 React 组件中。我们创建一个具有className属性和一些文本作为其子元素的h1元素。然后,我们在调用render()方法时返回它。我们将 React 元素封装在 React 组件中这一事实并不影响它的渲染方式:

<h1 data-reactroot class="header">React Component</h1>

如您所见,生成的 HTML 标记与我们在第 3 章中创建的标记相同创建第一个 React 元素,而不使用 React 组件。在这种情况下,您可能会想,如果我们可以在没有render()方法的情况下呈现完全相同的标记,那么使用render()方法有什么好处?

使用render()方法的优点是,与任何其他函数一样,在返回值之前,它可以选择要返回的值。到目前为止,您已经看到了两个render()方法的示例:一个返回null,另一个返回 React 元素。我们可以将两者合并,并添加一个决定渲染内容的条件:

class ReactClass extends Component {
  render() {
    const componentState = {
      isHidden: true
    };

    if (componentState.isHidden) {
      return null;
    }

    return (
      <h1 className="header">React Component</h1>
    );
  }
}

在本例中,我们创建了引用具有单个isHidden属性的对象的componentState常量。此对象充当 React 组件的状态。如果我们想隐藏我们的反应成分,那么我们需要将componentState.isHidden的值设置为true,我们的render函数将返回null。在这种情况下,React 将不渲染任何内容。从逻辑上讲,将componentState.isHidden设置为false,将返回 React 元素并呈现预期的 HTML 标记。您可能会问,我们如何将componentState.isHidden的值设置为false?还是到true?或者我们通常如何改变它?

让我们想想我们可能想要改变这种状态的场景。其中之一是当用户与我们的用户界面交互时。另一个是服务器发送数据时。或者,当一段时间过去了,现在,我们想要渲染其他东西。我们的render()方法不知道所有这些事件,也不应该知道,因为它的唯一目的是基于我们传递给它的数据返回 React 元素。我们如何向它传递数据?

有两种方法可以使用 React API 将数据传递给render()方法:

  • this.props
  • this.state

这里,this.props对你来说应该很熟悉。在第三章创建您的第一个 React 元素中,您了解到React.createElement()函数接受props参数。我们使用它将属性传递给 HTML 元素,但我们没有讨论场景后面会发生什么,以及为什么传递给props对象的属性会被渲染。

您放入props对象并传递给 JSX 元素的任何数据都可以通过this.propsrender()方法内部访问。一旦您从this.props访问数据,您就可以呈现它:

class ReactClass extends Component {
  render() {
    const componentState = {
      isHidden: false
    };

    if (componentState.isHidden) {
      return null;
    }

    return (
      <h1 className="header">{this.props.header}</h1>
    );
  }
}

在本例中,我们在render()方法中使用this.props来访问header属性。然后,我们将把this.props.header作为一个孩子直接传递给h1 element

在上例中,我们可以将isHidden的值作为this.props对象的另一个属性传递:

class ReactClass extends Component {
  render() {
    if (this.props.isHidden) {
      return null;
    }

    return (
      <h1 className="header">{this.props.header}</h1>
    );
  }
}

注意,在这个例子中,我们重复了两次this.props。一个this.props对象有我们想在render方法中多次访问的属性是很常见的。因此,我建议您首先对this.props进行分解:

class ReactClass extends Component {
  render() {
    const {
      isHidden,
      header
    } = this.props;

    if (isHidden) {
      return null;
    }

    return (
      <h1 className="header">{this.header}</h1>
    );
  }
}

您是否注意到,在上一个示例中,我们不是通过render()方法存储isHidden,而是通过this.props传递它?我们删除了componentState对象,因为在render()方法中我们不需要担心组件的状态。render()方法不应该改变组件的状态或访问真实的 DOM,或者与 web 浏览器交互。我们可能希望在没有 web 浏览器的服务器上呈现 React 组件,并且我们应该期望render()方法在不考虑环境的情况下产生相同的结果。

如果我们的render()方法无法管理状态,那么我们如何管理它?如何设置状态,以及如何在 React 中处理用户或浏览器事件时更新状态?

在本章前面,您了解到在 React 中,我们可以用 React 组件表示用户界面。有两种类型的 React 组件:

  • 与一个国家
  • 没有国家

等等我们不是说过 React 组件是状态机吗?当然,每个状态机都需要有一个状态。您是正确的,但是,将尽可能多的 React 组件保持为无状态是一个好的做法。

React 组件是可组合的。因此,我们可以有一个 React 组件的层次结构。假设我们有一个父 React 组件,它有两个子组件,而每个子组件又有另外两个子组件。所有组件都是有状态的,它们可以管理自己的状态:

Creating your first stateless React component

如果层次结构中的顶部组件更新其状态,那么确定层次结构中最后一个子组件将渲染什么将有多容易?不容易。有一种设计模式可以消除这种不必要的复杂性。这个想法是通过两个关注点来分离组件:如何处理用户界面交互逻辑和如何呈现数据。

  • 少数 React 组件是有状态的。它们应该位于组件层次结构的顶部。它们封装了所有的交互逻辑,管理用户界面状态,并使用props将该状态沿层次结构传递给无状态组件。
  • 大多数 React 组件都是无状态的。它们通过this.props接收其父组件的状态数据,并相应地呈现该数据。

在前面的示例中,我们通过this.props接收isHidden状态数据,然后呈现该数据。我们的组件是无状态的。

接下来,让我们创建第一个有状态组件。

创建第一个有状态的 React 组件

有状态组件是应用处理交互逻辑和管理状态的最合适位置。它们使您更容易对应用的工作方式进行推理。这种推理在构建可维护的 web 应用中起着关键作用。

React 将组件的状态存储在this.state对象中。我们将this.state的初始值指定为Component类的公共类字段:

class ReactClass extends React.Component {
  state = {
    isHidden: false
  };

  render() {
    const {
      isHidden
    } = this.state;

    if (isHidden) {
      return null;
    }

    return (
      <h1 className="header">React Component</h1>
    );
  }
}

现在,{ isHidden: false }是 React 组件和用户界面的初始状态。注意,在我们的render()方法中,我们现在从this.state而不是this.props解构isHidden属性。

在本章前面,您了解到我们可以通过this.propsthis.state将数据传递给组件的render()函数。这两者的区别是什么?

  • this.props:存储从父级传递的只读数据。它属于父级并且不能被其子级更改。这些数据应该被认为是不可变的。
  • this.state:存储组件私有的数据。它可以由组件更改。当状态更新时,组件将重新呈现自身。

我们如何更新组件的状态?您可以使用setState(nextState, callback)通知 React 状态更改。此函数采用两个参数:

  • 表示下一个状态的nextState对象。它也可以是一个签名为function(prevState, props) => newState的函数。此函数接受两个参数:previous state 和 properties,并返回表示新状态的对象。
  • callback功能,您很少需要使用它,因为 React 可以让您的用户界面保持最新。

React 如何使您的用户界面保持最新?每次您更新组件的状态时,它都会调用组件的render()函数,包括重新呈现的所有子组件。事实上,每次调用render()函数时,它都会重新呈现整个虚拟 DOM。

当您调用this.setState()函数并向其传递表示下一个状态的数据对象时,React 会将该下一个状态与当前状态合并。在合并过程中,React 将用下一个状态覆盖当前状态。未被下一状态覆盖的当前状态将成为下一状态的一部分。

假设这是我们当前的状态:

{
  isHidden: true,
  title: 'Stateful React Component'
}

我们称之为this.setState(nextState),其中nextState如下:

{
  isHidden: false
}

React 将两个状态合并为一个新状态:

{
  isHidden: false,
  title: 'Stateful React Component'
}

isHidden属性已更新,title属性未被删除或以任何方式更新。

现在我们知道了如何更新组件的状态,让我们创建一个对用户事件做出反应的有状态组件:

在本例中,我们将创建一个显示和隐藏标题的切换按钮。我们要做的第一件事是设置初始状态对象。我们的初始状态有两个属性:isHeaderHidden设置为false,标题设置为Stateful React Component。现在,我们可以通过this.staterender()方法中访问此状态对象。在我们的render()方法中,我们创建了三个反应元素:h1buttondiv。我们的div元素充当h1button元素的父元素。但是,在一种情况下,我们创建的div元素有两个子元素headerbutton元素,而在另一种情况下,我们只创建了一个子元素button。我们选择的案例取决于this.state.isHeaderHidden的值。组件的当前状态直接影响render()函数将呈现的内容。虽然这对您来说应该很熟悉,但在这个示例中有一些我们以前没有见过的新内容。

注意,我们在组件类中添加了一个名为handleClick()的新方法。handleClick()方法对反应没有特殊意义。它是我们应用逻辑的一部分,我们使用它来处理onClick事件。您也可以将自己的自定义方法添加到 React 组件类中,因为它只是一个 JavaScript 类。所有这些方法都可以通过this引用获得,您可以在组件类中的任何方法中访问该引用。例如,我们通过render()handleClick()方法中的this.state访问状态对象。

我们的handleClick()方法做什么?它通过切换isHeaderHidden属性来更新组件的状态:

this.setState(prevState => ({
  isHeaderHidden: !prevState.isHeaderHidden
}));

我们的handleClick()方法对用户与我们的用户界面的交互作出反应。我们的用户界面是一个button元素,用户可以点击它,我们可以向它附加一个事件处理程序。在 React 中,您可以通过将事件处理程序传递给 JSX 属性将其附加到组件:

<button onClick={this.handleClick}>
  Toggle Header
</button>

React 对事件处理程序使用camelCase命名约定,例如onClick。您可以在找到所有受支持事件的列表 http://facebook.github.io/react/docs/events.html#supported-事件

默认情况下,React 会在冒泡阶段触发事件处理程序,但您可以通过在事件名称后面添加Capture来告知 React 会在捕获阶段触发它们,例如onClickCapture

React 将浏览器的本机事件包装到SyntheticEvent对象中,以确保所有受支持的事件在 Internet Explorer 8 及更高版本中的行为相同。

SyntheticEvent对象提供与本机浏览器事件相同的 API,这意味着您可以像往常一样使用stopPropagation()preventDefault()方法。如果出于某种原因,您需要访问该本机浏览器的事件,那么您可以通过nativeEvent属性进行访问。

请注意,在前面的示例中,将onClick属性传递给我们的createElement()函数不会在呈现的 HTML 标记中创建内联事件处理程序:

<button class="btn btn-default">Toggle header</button>

这是因为 React 实际上并不将事件处理程序附加到 DOM 节点本身。相反,React 使用单个事件侦听器在顶层侦听所有事件,并将它们委托给相应的事件处理程序。

在前面的示例中,您学习了如何创建一个有状态的 React 组件,用户可以与之交互并更改其状态。我们为click事件创建并附加了一个事件处理程序,用于更新isHeaderHidden属性的值。但是您是否注意到,用户交互不会更新我们存储在状态中的另一个属性title的值。你觉得奇怪吗?我们所在州的数据从未改变过。这一观察提出了一个重要问题;我们不应该在我们的州放什么?

问问自己,“我可以从组件的状态中删除哪些数据,并使其用户界面始终保持最新?”继续询问并不断删除这些数据,直到您完全确定没有什么可以删除,而不会破坏您的用户界面。

在我们的示例中,我们的状态对象中有title属性,我们可以移动到render()方法,而不会破坏切换按钮的交互性。该组件仍将按预期工作:

class ReactClass extends Component {
  state = {
    isHeaderHidden: false
  }

  handleClick = () => {
    this.setState(prevState => ({
      isHeaderHidden: !prevState.isHeaderHidden
    }));
  }

  render() {
    const {
      isHeaderHidden
    } = this.state;

    if (isHeaderHidden) {
      return (
        <button
          className="btn ban-default"
          onClick={this.handleClick}
        >
          Toggle Header
        </button>
      );
    }

    return (
      <div>
        <h1 className="header">Stateful React Component</h1>
        <button
          className="btn ban-default"
          onClick={this.handleClick}
        >
          Toggle Header
        </button>
      </div>
    );
  }
}

另一方面,如果我们将isHeaderHidden属性移出状态对象,那么我们将破坏组件的交互性,因为我们的render()方法不会在用户每次单击我们的按钮时自动触发。这是一个中断交互的示例:

class ReactClass extends Component {
  state = {}
  isHeaderHidden = false

  handleClick = () => {
    this.isHeaderHidden = !this.isHeaderHidden;
  }

  render() {
    if (this.isHeaderHidden) {
      return (
        <button
          className="btn ban-default"
          onClick={this.handleClick}
        >
          Toggle Header
        </button>
      );
    }

    return (
      <div>
        <h1 className="header">Stateful React Component</h1>
        <button
          className="btn ban-default"
          onClick={this.handleClick}
        >
          Toggle Header
        </button>
      </div>
    );
  }
}

:为了更好的输出结果,请参考代码文件。

这是一个反模式。

记住这个经验法则:组件的状态应该存储组件的事件处理程序可能随时间改变的数据,以便重新呈现组件的用户界面并使其保持最新。在state对象中保留组件状态的最小可能表示,并根据组件render()方法中的stateprops计算其余数据。利用 React 将在组件状态更改时重新呈现组件这一事实。

总结

在本章中,您达到了一个重要的里程碑:您学习了如何通过创建 React 组件来封装状态和创建交互式用户界面。我们讨论了无状态和有状态的 React 组件以及它们之间的区别。我们讨论了浏览器事件以及如何在 React 中处理它们。

在下一章中,您将了解 React 16 的新增功能。