第 4 章 创建您的第一个 React 组件

第 4 章 创建您的第一个 React 组件

Flying
2018-08-25 / 0 评论 / 550 阅读 / 正在检测是否收录...

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

无状态与有状态

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

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

创建第一个无状态 React 组件

让我们看一看以下如何创建 React 组件的示例:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class ReactClass extends Component {
  render() {
    return <h1 className="header">React Component</h1>;
  }
}
const reactComponent = ReactDOM.render(
  <ReactClass />,
  document.getElementById('react-application')
);

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

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

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

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

我强烈建议你阅读 Dan Abramov 的 这篇博文,该博文更详细地解释了 React 组件、元素和实例之间的差异:

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

Component 类封装组件的状态,并描述如何渲染组件。至少 ReactClass 类需要有一个 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 组件中。我们创建了一个 h1 元素,该元素具有 className 属性和一些文本作为其子元素。然后,我们在调用 render() 方法时返回它。事实上,我们将 React 元素封装在 React 组件中并不影响其渲染方式:

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

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

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

class class ReactClass extends Component {
  render() {
    const componentState = {
      isHidden: true
    };
    if (componentState.isHidden) {
      return null;
    }
    return <h1 className="header">React Component</h1>;
  }
}

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

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

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

  • this.props
  • this.state

此处 this.props 看起来应该很熟悉。在第 3 章创建你的第一个 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 元素。

在前面的示例中,我们可以将 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 对象具有我们希望在渲染方法中多次访问的属性。出于这个原因,我建议你提前解构 this.props
class ReactClass extends Component {
  render() {
    const { isHidden, header } = this.props;
    if (isHidden) {
      return null;
    }
    return <h1 className="header">{this.header}</h1>;
  }
}

你是否注意到,在前面的示例中,我们不是将 isHidden 存储在 render() 方法中,而是通过 this.props 传递它?我们删除了 componentState 对象,因为我们不需要担心 render() 方法中组件的状态。 render() 方法不应改变组件的状态或访问真实的 DOM,也不应与 web 浏览器交互。我们可能希望在没有 web 浏览器的服务器上渲染 React 组件,期望 render() 方法无论环境如何都能产生相同的结果。

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

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

  • 有状态
  • 没有状态

等等我们不是说过 React 组件是状态机吗?每个状态机都需要有一个状态。你是对的,但保持尽可能多的 React 组件无状态是一个很好的做法。

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

compose.jpg

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

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

在上一个示例中,我们通过 this.props 接收 isHidden 状态数据,然后我们渲染这些数据。我们的组件是无状态的。

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

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

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

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

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'
}

我们调用 setState(nextState),其中 nextState 如下:

{
  isHidden: false;
}

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

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

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

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

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

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

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

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

handleClick 方法响应用户与用户界面的交互。我们的用户界面是一个 button 元素,用户可以点击它,我们可以通过设置组件 JSX 属性附加事件处理函数:

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

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

默认情况下,React 在冒泡阶段触发事件处理程序,但你可以通过在事件名称后附加捕获(例如,onClickCapture)来告诉 React 在捕获阶段触发它们。

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() 方法不会在每次用户单击我们的按钮时由 React 自动触发。这是一个破坏交互性的例子:

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 中处理它们。

下一章你将学习如何构建响应式组件。

7

评论 (0)

取消