React16-高级教程-五-

93 阅读27分钟

React16 高级教程(五)

原文:Pro React 16

协议:CC BY-NC-SA 4.0

十、组件和属性

在这一章中,我描述了 React 应用中的关键构件:组件。在本章中,我主要关注最简单的组件类型,即无状态组件。在第十一章中,我描述了更复杂的替代方案,有状态组件。在本章中,我还解释了 props 特性是如何工作的,它允许一个组件向另一个组件提供呈现其内容所需的数据,以及在发生重要事情时应该调用的函数。表 10-1 将无状态组件和属性放在上下文中。

表 10-1

将无状态组件和属性放在上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | 组件是 React 应用中的关键构件。无状态组件是 JavaScript 函数,它呈现 React 可以呈现给用户的内容。Props 是一个组件向另一个组件提供数据的方式,这样它就可以调整它所呈现的内容。 | | 它们为什么有用? | 组件很有用,因为它们通过组合 JavaScript、HTML 和其他组件来提供对创建特性的 React 支持的访问。属性很有用,因为它们允许组件修改它们产生的内容。 | | 它们是如何使用的? | 无状态组件被定义为返回 React 元素的 JavaScript 函数,通常使用 JSX 格式的 HTML 来定义。属性被定义为元素的属性。 | | 有什么陷阱或限制吗? | React 要求组件以特定的方式运行,例如返回单个 React 元素并总是返回一个结果,并且需要一段时间来适应这些限制。props 最常见的缺陷是在需要 JavaScript 表达式时指定文字值。 | | 还有其他选择吗? | 组件是 React 应用中的关键构建块,没有办法避免使用它们。正如在第十四章和第三部分中所描述的,在更大更复杂的项目中,props 有其他的选择。 |

表 10-2 总结了本章内容。

表 10-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 向 React 应用添加内容 | 定义一个返回 HTML 元素或调用React.createElement方法的函数 | 1–9 | | 向 React 应用添加附加功能 | 定义组件,并使用与组件名称相对应的元素将它们组合成父子关系 | 10–14 | | 配置子组件 | 应用组件时定义属性 | 15–19 | | 为数据数组中的每个对象呈现 HTML 元素 | 使用 map 方法创建元素,确保它们有一个key属性 | 20–24 | | 从一个组件呈现多个元素 | 使用React.Fragment元素或使用不带标签的元素 | 25–28 | | 不呈现任何内容 | 返回null | Twenty-nine | | 从子组件接收通知 | 用功能属性配置组件 | 31–34 | | 给孩子传递属性 | 使用从父代接收的属性值或使用析构运算符 | 35–39 | | 定义默认属性值 | 使用defaultProps属性 | 40, 41 | | 检查属性类型 | 使用propTypes属性 | 42–44 |

为本章做准备

为了创建本章的示例项目,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 10-1 中所示的命令。

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

npx create-react-app components

Listing 10-1Creating the Example Project

运行清单 10-2 中所示的命令,导航到项目文件夹,并将引导包添加到项目中。

cd components
npm install bootstrap@4.1.2

Listing 10-2Adding the Bootstrap CSS Framework

为了在应用中包含引导 CSS 样式表,将清单 10-3 中所示的语句添加到index.js文件中,该文件可以在src文件夹中找到。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'bootstrap/dist/css/bootstrap.css';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

Listing 10-3Including Bootstrap in the index.js File in the src Folder

使用命令提示符,运行components文件夹中清单 10-4 所示的命令来启动开发工具。

npm start

Listing 10-4Starting the Development Tools

一旦项目的初始准备工作完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000并显示如图 10-1 所示的占位符内容。

img/473159_1_En_10_Fig1_HTML.jpg

图 10-1

运行示例应用

了解组件

从组件开始的最佳方式是定义一个组件,然后看看它是如何工作的。在清单 10-5 中,我用一个简单的组件替换了App.js文件的内容。

export default function App() {
    return "Hello Adam";
}

Listing 10-5Defining a Component in the App.js File in the src Folder

这是一个无状态组件的例子,它就像一个组件一样简单:一个函数返回 React 将显示给用户的内容,这被称为呈现。当应用启动时,index.js文件中的代码被执行,包括呈现App组件的语句。React 调用该函数并将结果显示给用户,如图 10-2 所示。

img/473159_1_En_10_Fig2_HTML.jpg

图 10-2

定义和应用组件

尽管结果可能很简单,但它揭示了组件的主要用途,即向用户提供 React 显示内容。

呈现 HTML 内容

当组件呈现一个字符串值时,它作为文本内容包含在父元素中。当组件返回 HTML 内容时,它们变得更加有用,利用 JSX 和它允许 HTML 与 JavaScript 代码混合的方式,这是最容易做到的。在清单 10-6 中,我更改了组件的结果,使其呈现 HTML 的一个片段。

小费

使用 JSX 时,必须从react模块声明对 React 的依赖,如清单所示。如果你忘记了,你会收到一个警告。

import React from "react";

export default function App() {
    return  <h1 className="bg-primary text-white text-center p-2">
                Hello Adam
            </h1>
}

Listing 10-6Rendering HTML in the App.js File in the src Folder

您记得在组件的函数中使用return关键字来呈现结果。这可能会令人感到尴尬,但是请记住,JSX 文件中的 HTML 片段被转换为对createElement方法的调用,这将产生一个 React 可以向用户显示的对象。

当您考虑在构建过程中用createElement方法替换 HTML 片段后代码看起来是什么样时,使用return关键字是有意义的。

...
import React from "react";

export default function App() {
    return React.createElement("h1",
                { className: "bg-primary text-white text-center p-2" },
                "Hello Adam");
}
...

component 函数返回来自React.createElement方法的结果,该方法是 React 可以用来向域对象模型(DOM)添加内容的元素。

如果你想在与关键字return不同的一行开始 HTML,那么你可以用括号将结果括起来,如清单 10-7 所示。

import React from "react";

export default function App() {
    return (
        <h1 className="bg-primary text-white text-center p-2">
            Hello Adam
        </h1>
    )
}

Listing 10-7Using Parentheses in the App.js File in the src Folder

这允许 HTML 元素一致地缩进,尽管悬空的()字符可能会让一些开发人员感到别扭。

功能组件也可以使用粗箭头语法来定义,该语法省略了关键字return,如清单 10-8 所示。

import React from "react";

export default () =>

    <h1 className="bg-primary text-white text-center p-2">
        Hello Adam
    </h1>

Listing 10-8Using a Fat Arrow Function in the App.js File in the src Folder

粗箭头函数在没有名称的情况下导出,这在示例应用中有效,因为从App.js文件导入组件的index.js文件中的语句使用默认导出,如下所示:

...
import App from './App';
...

按名称导出一个胖箭头函数并作为缺省值需要一个额外的语句,如清单 10-9 所示。

import React from "react";

 export const App = () =>
    <h1 className="bg-primary text-white text-center p-2">
        Hello Adam
    </h1>

export default App;

Listing 10-9Creating a Named and Default Export in the App.js File in the src Folder

粗箭头函数被分配给一个按名称导出的const,一个单独的语句使用该名称创建缺省导出,这允许组件按名称导入并作为缺省值。

注意

我之所以包括这个例子,是因为模块导出会引起混淆,但是在实际项目中,它们要么使用命名导出,要么使用默认导出,并且不需要适应两种工作方式。我更喜欢使用命名导出,这是我在本书的例子中采用的方法。

我在这一章中使用了常规函数,并在有助于使 HTML 内容更具可读性的地方使用了括号,但是这一节中的所有例子都产生了相同的结果,如图 10-3 所示。

img/473159_1_En_10_Fig3_HTML.jpg

图 10-3

返回 HTML 内容

渲染其他组件

React 最重要的特性之一是,由一个组件呈现的内容可以包含其他组件,从而允许组合这些特性来创建复杂的应用。我在src文件夹中添加了一个名为Message.js的文件,并用它来定义清单 10-10 中所示的组件。

import React from "react";

export function Message() {
    return  <h4 className="bg-success text-white text-center p-2">
                This is a message
            </h4>
}

Listing 10-10The Contents of the Message.js File in the src Folder

Message组件呈现一个包含消息的h4元素。在清单 10-11 中,我已经更新了App组件,以便它将Message内容作为其内容的一部分呈现。

import React from "react";

import { Message } from "./Message";

export default function App() {

    return  (
        <div>
            <h1 className="bg-primary text-white text-center p-2">
                Hello Adam
            </h1>
            <Message />
        </div>
    )
}

Listing 10-11Rendering Another Component in the App.js File in the src Folder

import语句声明了对Message组件的依赖,该组件使用Message元素呈现。当 React 接收到由App组件呈现的内容时,它将包含Message元素,它将通过调用Message组件的函数并用其呈现的内容替换Message元素来处理该元素,产生如图 10-4 所示的结果。

img/473159_1_En_10_Fig4_HTML.jpg

图 10-4

呈现其他内容

当一个组件像这样使用另一个组件时,就形成了父子关系。在这个例子中,App组件是Message组件的父组件,而Message组件是App组件的子组件。通过为子组件定义多个元素,一个组件可以多次应用同一个组件,如清单 10-12 所示。

import React from "react";
import { Message } from "./Message";

export default function App() {
    return  (
        <div>
            <h1 className="bg-primary text-white text-center p-2">
                Hello Adam
            </h1>
            <Message />
            <Message />
            <Message />
        </div>
    )
}

Listing 10-12Applying a Child Component in the App.js File in the src Folder

每当 React 遇到Message元素时,它就会调用Message组件,并使用其呈现的内容来替换Message元素,如图 10-5 所示。

img/473159_1_En_10_Fig5_HTML.jpg

图 10-5

应用多个子项

一个组件可以有不同类型的子组件,这意味着一个组件可以利用多个组件提供的功能。我用清单 10-13 中所示的代码,通过将名为Summary.js的文件添加到src文件夹中,创建了另一个简单的组件。

import React from "react";

export function Summary() {
    return  <h4 className="bg-info text-white text-center p-2">
                This is a summary
            </h4>
}

Listing 10-13The Contents of the Summary.js File in the src Folder

在清单 10-14 中,我更新了App组件以声明对Summary组件的依赖,并使用Summary元素呈现其内容。

import React from "react";
import { Message } from "./Message";

import { Summary } from "./Summary";

export default function App() {
    return (
        <div>
            <h1 className="bg-primary text-white text-center p-2">
                Hello Adam
            </h1>
            <Message />
            <Message />
            <Message />
            <Summary />
        </div>
    )
}

Listing 10-14Adding a Child Component in the App.js File in the src Folder

当 React 处理由App组件呈现的内容时,它遇到子组件的元素,调用它们的函数,并用它们呈现的内容替换MessageSummary元素。结果如图 10-6 所示。

img/473159_1_En_10_Fig6_HTML.jpg

图 10-6

使用不同的子组件

理解属性

当每个组件呈现相同的内容时,能够呈现来自多个子组件的内容就没有那么有用了。幸运的是,React 支持props——properties的缩写——它允许父组件向其子组件提供数据,子组件可以使用这些数据来呈现它们的内容。在接下来的章节中,我将解释属性是如何工作的,并演示它们的不同用法。

在父组件中定义属性

通过向应用组件的自定义 HTML 元素添加属性来定义属性。属性的名称是属性的名称,值可以是静态值或表达式。在清单 10-15 中,我为App组件使用的Message元素添加了属性。

import React from "react";
import { Message } from "./Message";
import { Summary } from "./Summary";

export default function App() {
    return  (
        <div>
            <h1 className="bg-primary text-white text-center p-2">
                Hello Adam
            </h1>
            <Message greeting="Hello" name="Bob" />
            <Message greeting="Hola" name={ "Alice" + "Smith" } />
            <Message greeting="Hi there" name="Dora" />
            <Summary />
        </div>
    )
}

Listing 10-15Defining Props in the App.js File in the src Folder

我为每个Message组件提供了两个属性greetingname。大多数属性值都是静态值,用文字字符串表示。第二个Message元素上的greeting属性的值是一个表达式,它连接了两个字符串值。(您将看到一个关于清单 10-15 中表达式的 linter 警告,因为串联字符串文字值是 linter 配置要检测的不良实践之一。在本章中,可以忽略棉绒警告。)

定义属性

Props 可用于将静态值或动态表达式的结果传递给子组件。静态值按字面引用,就像这样:

...
<Message greeting="Hello" name="Bob" />
...

这个属性为子组件的name属性提供了值Bob。如果您想使用 JavaScript 表达式的结果作为属性的值,那么使用数据绑定表达式,如下所示:

...
<Message greeting="Hola" name={ "Alice" + "Smith" } />
...

React 将计算表达式,并将结果(在本例中是两个字符串的连接)用作属性的值。一个常见的错误是将 JavaScript 表达式放在引号中,就像这样:

...
<Message greeting="Hola" name="{ "Alice" + "Smith" }" />
...

React 会将此解释为使用静态值{ "Alice" + "Smith" }作为属性值的请求。用表情做属性的时候,一定要记得不要用引号。如果您不想使用 JSX,而想使用纯 JavaScript 创建 React 元素,那么 props 作为第二个参数提供给createElement方法,如下所示:

...
React.createElement(Message, { greeting: "Hola",  name: "Alice" + "Smith"})
...

如果你没有得到你期望的结果,在 JSX 或纯 JavaScript 中,React Devtools 浏览器扩展(在第九章中描述)可以显示应用中每个组件收到的属性,这使得很容易看到哪里出错了。

在子组件中接收属性

通过定义一个名为props的参数,Props 被接收到组件中(尽管这只是一个约定,您可以给参数取任何合法的 JavaScript 名称)。props对象对于每个属性都有一个属性,该属性被赋予了属性值。举个例子,这些属性来自清单 10-15 :

...
<Message greeting="Hello" name="Bob" />
...

会被翻译成这样一个物体:

...
{
    greeting: "Hello",
    name: "Bob"
}
...

在清单 10-16 中,我修改了Message组件,这样它定义了一个属性参数,并在结果中使用父组件提供的值。

import React from "react";

export function Message(props) {

    return  <h4 className="bg-success text-white text-center p-2">
                {props.greeting}, {props.name}
            </h4>
}

Listing 10-16Using Props in the Message.js File in the src Folder

子组件不需要担心属性值是静态指定的还是用表达式指定的,它像其他 JavaScript 对象一样使用这些属性。在清单中,我在表达式中使用了greetingname属性来设置组件呈现的h4元素的内容,产生了如图 10-7 所示的结果。

img/473159_1_En_10_Fig7_HTML.jpg

图 10-7

使用属性渲染内容

结合 JavaScript 和 Props 来呈现内容

清单 10-16 中的App组件定义的每个Message元素的属性值会产生不同的内容,允许父组件以不同的方式使用相同的功能。

选择性呈现内容

组件可以使用 JavaScript if关键字来检查属性,并根据其值呈现不同的内容。在清单 10-17 中,我使用了if语句来改变由Message组件呈现的内容。

import React from "react";

export function Message(props) {
    if (props.name === "Bob") {
        return  <h4 className="bg-warning p-2">{props.greeting}, {props.name}</h4>
    } else {
        return  <h4 className="bg-success text-white text-center p-2">
                {props.greeting}, {props.name}
            </h4>
    }
}

Listing 10-17Selectively Rendering in the Message.js File in the src Folder

如果name属性的值为Bob,组件将呈现一个具有不同类成员关系的h4元素,如图 10-8 所示。

img/473159_1_En_10_Fig8_HTML.jpg

图 10-8

使用 if 语句选择内容

这种类型的选择性呈现,其中只有属性的值发生变化,可以通过将属性的值从 HTML 的其余部分中分离出来,以更少的重复来表示,如清单 10-18 所示。

import React from "react";

export function Message(props) {

    let classes = props.name === "Bob" ? "bg-warning p-2"
        : "bg-success text-white text-center p-2";

    return  <h4 className={ classes }>
                {props.greeting}, {props.name}
            </h4>
}

Listing 10-18Selecting a Property Value in the Message.js File in the src Folder

我已经使用了 JavaScript 三元条件操作符来选择将分配给h4元素的类,并使用一个用于className属性的表达式来应用这些类。结果与清单 10-17 相同,但是没有复制 HTML 元素的不变部分。

当一个组件需要从一个更复杂的列表中选择内容时,可以使用一个switch语句,如清单 10-19 所示。

import React from "react";

export function Message(props) {
    let classes;
    switch (props.name) {
        case "Bob":
            classes = "bg-warning p-2";
            break;
        case "Dora":
            classes = "bg-secondary text-white text-center p-2"
            break;
        default:
            classes = "bg-success text-white text-center p-2"
    }
    return  <h4 className={ classes }>
                {props.greeting}, {props.name}
            </h4>
}

Listing 10-19Using a switch Statement in the Message.js File in the src Folder

这个例子使用props.name值上的switch语句来选择h4元素的类,产生如图 10-9 所示的结果。

img/473159_1_En_10_Fig9_HTML.jpg

图 10-9

使用 switch 语句选择内容

渲染数组

组件通常必须为数组中的每个元素创建 HTML 元素,通常以列表或表格中的行的形式显示项目。处理数组所需的技术会引起混淆,值得仔细研究。为了做好准备,我更新了App组件,以便它用一个属性配置Summary组件,如清单 10-20 所示。(为了保持示例简单,我还删除了一些元素。)

import React from "react";

//import { Message } from "./Message";

import { Summary } from "./Summary";

export default function App() {
    return  (
        <div>
            <h1 className="bg-primary text-white text-center p-2">
                Hello Adam
            </h1>
            <Summary names={ ["Bob", "Alice", "Dora"]} />
        </div>
    )
}

Listing 10-20Adding a Prop in the App.js File in the src Folder

names属性为Summary组件提供了一个字符串值数组。在清单 10-21 中,我修改了由Summary组件呈现的内容,这样它就可以为数组中的每个值生成元素。

import React from "react";

function createInnerElements(names) {

    let arrayElems  = [];
    for (let i = 0; i < names.length; i++) {
        arrayElems.push(
            <div>
                {`${names[i]} contains ${names[i].length} letters`}
            </div>
        )
    }
    return arrayElems;

}

export function Summary(props) {
    return  <h4 className="bg-info text-white text-center p-2">
                { createInnerElements(props.names)}
            </h4>
}

Listing 10-21Rendering an Array in the Summary.js File in the src Folder

组件函数使用一个表达式来设置h4元素的内容,这是通过调用createInnerElements函数来完成的。createInnerElements函数使用 JavaScript for循环来枚举names数组的内容,并将div元素添加到结果数组中。

...
arrayElems.push(<div>{`${names[i]} contains ${names[i].length} letters`}</div>)
...

每个div元素的内容由另一个表达式设置,该表达式使用模板字符串创建特定于数组中每个元素的消息。div元素的数组作为createInnerElements函数的结果返回,并用作h4元素的内容,产生如图 10-10 所示的结果。

img/473159_1_En_10_Fig10_HTML.jpg

图 10-10

为数组中的对象创建 React 元素

使用映射方法处理数组对象

虽然for循环是大多数程序员用来枚举数组的方式,但它并不是 React 中处理数组的最优雅的方式。在第四章中描述的map方法可以用来将数组中的对象转换成 HTML 元素,如清单 10-22 所示。

import React from "react";

function createInnerElements(names) {
    return names.map(name =>
        <div>
            {`${name} contains ${name.length} letters`}
        </div>
    )
}

export function Summary(props) {
    return  <h4 className="bg-info text-white text-center p-2">
                { createInnerElements(props.names)}
            </h4>
}

Listing 10-22Transforming an Array in the Summary.js File in the src Folder

map方法的参数是为数组中的每个对象调用的函数。每次调用传递给 map 方法的函数时,数组中的下一项都会传递给函数,我用它来创建表示该对象的元素。每次调用该函数的结果都被添加到一个数组中,该数组用作map结果。清单 10-22 中的代码产生与清单 10-21 相同的结果。

小费

您不必在map方法中使用粗箭头函数,但是它会产生一个更简洁的组件。

既然createInnerElement函数包含一行代码,我可以通过将创建内部元素的语句移到组件函数中来进一步简化组件,如清单 10-23 所示。

import React from "react";

export function Summary(props) {
    return (
        <h4 className="bg-info text-white text-center p-2">
            {   props.names.map(name =>
                    <div>
                        {`${name} contains ${name.length} letters`}
                    </div>
                )
            }
        </h4>
    )
}

Listing 10-23Simplifying the Code in the Summary.js File in the src Folder

这种改变不会改变输出,并且产生与清单 10-21 和清单 10-22 相同的结果。

使用 Map 方法时接收其他参数

在清单 10-23 中,我传递给map方法的函数接收当前数组对象作为它的参数。map 方法还提供了两个附加参数:数组中当前对象的从零开始的索引和完整的对象数组。您可以在本章后面的“呈现多个元素”一节中看到数组索引的示例。

添加关键属性

要完成这个示例,还需要做最后一项更改。React 需要将一个key prop 添加到为数组中的对象生成的元素中,以便可以有效地处理变化,正如我在第十三章中解释的。属性的值应该是一个表达式,其值在数组中唯一地标识对象,如清单 10-24 所示。

import React from "react";

export function Summary(props) {
    return (
        <h4 className="bg-info text-white text-center p-2">
            {   props.names.map(name =>
                    <div key={ name }>
                        {`${name} contains ${name.length} letters`}
                    </div>
                )
            }
        </h4>
    )
}

Listing 10-24Adding the Key Prop in the Summary.js File in the src Folder

我使用了name变量的值,当传递给map方法的函数被调用时,数组中的每个对象都被分配给这个变量,它允许 React 区分从数组对象创建的元素。

React 将显示没有 key prop 的元素,如本节前面的示例所示,但是浏览器的 JavaScript 控制台中将显示一条警告。

呈现多个元素

React 要求组件返回单个顶级元素,尽管该元素可以包含应用所需的任意多个其他元素。例如,Summary组件返回一个顶级的h4元素,它包含一系列为names属性中的元素生成的div元素。

有时候,对单个顶级元素的需求会导致问题。HTML 规范对如何组合元素施加了限制,这可能与单个元素的 React 要求相冲突。为了演示这个问题,我修改了由App组件呈现的内容,使其包含一个表格,其中每个tr元素的内容由一个子组件生成,如清单 10-25 所示。

import React from "react";
import { Summary } from "./Summary";

let names = ["Bob", "Alice", "Dora"]

export default function App() {
    return  (
        <table className="table table-sm table-striped">
            <thead>
                <tr><th>#</th><th>Name</th><th>Letters</th></tr>
            </thead>
            <tbody>
                { names.map((name, index) =>
                        <tr key={ name }>
                            <Summary index={index} name={name} />
                        </tr>
                )}
            </tbody>
        </table>
    )
}

Listing 10-25Rendering a Table in the App.js File in the src Folder

Summary组件通过indexname属性。在清单 10-26 中,我已经更新了Summary组件,这样它就可以使用属性值生成一系列表格单元格。

import React from "react";

export function Summary(props) {
    return  <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
}

Listing 10-26Rendering Table Cells in the Summary.js File in the src Folder

Summary组件呈现一组td元素,因为这是 HTML 规范要求的td元素的子元素。但是当您保存更改时,您会看到以下错误:

...
Syntax error: src/Summary.js: Adjacent JSX elements must be wrapped
 in an enclosing tag (5:12)

  3 | export function Summary(props) {
  4 |     return  <td>{ props.index + 1} </td>
> 5 |             <td>{ props.name } </td>
    |             ^
  6 |             <td>{ props.name.length } </td>
  7 | }
...

该错误消息表明组件呈现的内容不符合单个顶级元素的 React 要求。没有一个 HTML 元素可以用来包装td元素,并且仍然是对表的合法添加。对于这些情况,React 提供了一个特殊的元素,如清单 10-27 所示。

import React from "react";

export function Summary(props) {
    return  <React.Fragment>
                <td>{ props.index + 1} </td>
                <td>{ props.name } </td>
                <td>{ props.name.length } </td>
            </React.Fragment>
}

Listing 10-27Wrapping Elements in the Summary.js File in the src Folder

React 处理Summary组件渲染的元素时,会丢弃React.Fragment元素,用剩余的内容替换应用了该组件的Summary元素,如图 10-11 所示。

img/473159_1_En_10_Fig11_HTML.jpg

图 10-11

呈现多个元素

对于这些情况,React 支持另一种语法,即使用不带标记名的封闭元素,如清单 10-28 所示。

import React from "react";

export function Summary(props) {
    return  <>
                <td>{ props.index + 1} </td>
                <td>{ props.name } </td>
                <td>{ props.name.length } </td>
            </>
}

Listing 10-28Wrapping Elements in the Summary.js File in the src Folder

这相当于列出 10-27 并产生相同的结果。我在本书的例子中使用了React.Fragment或者将多个元素包装在一个div中,这样就产生了 HTML 元素的合法组合。

不呈现任何内容

一个组件必须总是返回一个结果,即使它没有为 React 显示产生任何内容。在这些情况下,组件的函数应该返回null,在清单 10-29 中,我修改了Summary组件,这样当它的name属性的长度小于四个字符时,它就不会产生任何内容。

import React from "react";

export function Summary(props) {
    if (props.name.length >= 4) {
        return  <React.Fragment>
                    <td>{ props.index + 1} </td>
                    <td>{ props.name } </td>
                    <td>{ props.name.length } </td>
                </React.Fragment>
    } else {
        return null;
    }
}

Listing 10-29Rendering No Content in the Summary.js File in the src Folder

父组件仍然会应用Summary元素三次,每次都会导致Summary组件的函数被调用,但是只有两次调用会产生结果,如图 10-12 所示。

img/473159_1_En_10_Fig12_HTML.jpg

图 10-12

不呈现任何内容

试图改变属性

属性是只读的,不能被组件更改。当 React 创建props对象时,它会配置其属性,以便在进行任何更改时显示错误。在清单 10-30 中,我向Summary组件添加了一条语句,该语句更改了name属性的值。

import React from "react";

export function Summary(props) {
    props.name = `Name: ${props.name}`;
    if (props.name.length >= 4) {
        return  <React.Fragment>
                    <td>{ props.index + 1} </td>
                    <td>{ props.name } </td>
                    <td>{ props.name.length } </td>
                </React.Fragment>
    } else {
        return null;
    }
}

Listing 10-30Changing a Prop Value in the Summary.js File in the src Folder

当您保存更改并且浏览器重新加载时,您将看到如图 10-13 所示的错误消息。这是一个运行时错误,这意味着编译器在命令提示符下不会显示任何警告。

img/473159_1_En_10_Fig13_HTML.jpg

图 10-13

试图修改一个属性

小费

当使用第八章中描述的过程为部署构建应用时,不会显示此错误,这意味着您应该在开发期间进行彻底的测试,以确保您的组件不会无意中试图更改属性。

使用功能属性

到目前为止,我在本章中使用的所有属性都是数据属性,它为子组件提供了一个只读数据值。React 还支持函数 props,其中父组件为子组件提供一个函数,子组件可以调用该函数来通知父组件发生了重要的事情。父组件可以通过更改数据属性的值来响应,这将触发更新,并允许子组件向用户呈现更新的内容。

为了展示这是如何工作的,我在包含App组件的文件中定义了一个函数,它改变了用于Summary元素的name属性的值的顺序,如清单 10-31 所示。

import React from "react";
import { Summary } from "./Summary";

import ReactDOM from "react-dom";

let names = ["Bob", "Alice", "Dora"]

function reverseNames() {

    names.reverse();
    ReactDOM.render(<App />, document.getElementById('root'));

}

export default function App() {
    return  (
        <table className="table table-sm table-striped">
            <thead>
                <tr><th>#</th><th>Name</th><th>Letters</th></tr>
            </thead>
            <tbody>
                { names.map((name, index) =>
                    <tr key={ name }>
                        <Summary index={index} name={name}
                            reverseCallback={reverseNames} />
                    </tr>
                )}
            </tbody>
        </table>
    )
}

Listing 10-31Defining a Change Function in the App.js File in the src Folder

我定义的函数叫做reverseNames,它使用 JavaScript reverse方法来反转names数组中值的顺序。reverseNames函数作为名为reverseCallbackprop的值提供给Summary组件,如下所示:

...
<Summary index={index} name={name} reverseCallback={reverseNames} />
...

Summary组件将接收一个具有三个属性的 prop 对象:index prop 提供当前对象的索引,该对象由map方法处理,name prop 提供来自数组的当前值,reverseCallback prop 提供反转数组内容顺序的函数。在清单 10-32 中,我已经更新了Summary组件,以利用它作为属性接收的函数。(我还删除了试图改变属性值的语句,并删除了阻止组件呈现短名称内容的if语句。)

import React from "react";

export function Summary(props) {
    return (
        <React.Fragment>
            <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
            <td>
                <button className="btn btn-primary btn-sm"
                    onClick={ props.reverseCallback }>
                        Change
                </button>
            </td>
        </React.Fragment>
    )
}

Listing 10-32Using a Function Prop in the Summary.js File in the src Folder

该组件呈现一个button元素,其onClick属性选择它从其父元素接收的函数属性。我在第十二章中描述了onClick属性,但是,正如您在前面的章节中看到的,这个属性告诉 React 当用户点击一个元素时如何响应,在这种情况下,表达式告诉 React 调用reverseCallback属性,这是由父组件提供的功能。

结果是点击一个button元素导致 React 调用在App.js文件中定义的changeValues函数,这颠倒了用于name属性的值的顺序,产生如图 10-14 所示的结果。

img/473159_1_En_10_Fig14_HTML.jpg

图 10-14

使用作为属性接收的函数

理解 Update 语句

Summary组件调用函数 prop 时,调用reverseCallback函数,并执行清单 10-31 中的语句:

...
ReactDOM.render(<App />, document.getElementById('root'));
...

render方法用于将组件的内容添加到浏览器显示的文档对象模型(DOM)中,并在index.js文件中用于启动应用;在第十三章中有描述。这不是一个通常直接使用的特性,但是我需要能够执行更新来响应被调用的函数 prop。我在第十一章中描述了通常用于执行更新的特性。目前,只需知道调用这个方法会更新显示给用户的 HTML 元素,反映用于属性值的数据值的变化。

使用参数调用属性函数

在清单 10-32 中,onClick属性的表达式指定了函数 prop,如下所示:

...
<button className="btn btn-primary btn-sm" onClick={ props.reverseCallback } >
    Change
</button>
...

当函数被一个表达式选中时,它将被传递一个事件对象,我在第十二章中描述了这个对象,它提供了被调用的函数以及触发事件的 HTML 元素的细节。

这在调用 function prop 时并不总是有用的,因为它要求父组件对子组件有足够的了解才能理解事件并相应地采取行动。通常,更有帮助的方法是为函数提供一个自定义参数,直接向父组件提供它需要的细节。在清单 10-33 中,我向App.js文件添加了一个函数,将指定的名称移动到数组的前面,并更新了App组件,这样它就可以使用一个属性将函数传递给其子节点。

import React from "react";
import { Summary } from "./Summary";
import ReactDOM from "react-dom";

let names = ["Bob", "Alice", "Dora"]

function reverseNames() {
    names.reverse();
    ReactDOM.render(<App />, document.getElementById('root'));
}

function promoteName(name) {

    names = [name, ...names.filter(val => val !== name)];
    ReactDOM.render(<App />, document.getElementById('root'));

}

export default function App() {
    return (
        <table className="table table-sm table-striped">
            <thead>
                <tr><th>#</th><th>Name</th><th>Letters</th></tr>
            </thead>
            <tbody>
                { names.map((name, index) =>
                    <tr key={ name }>
                        <Summary index={index} name={name}
                            reverseCallback={reverseNames}
                            promoteCallback={promoteName} />
                    </tr>
                )}
            </tbody>
        </table>
    )
}

Listing 10-33Adding a Function in the App.js File in the src Folder

新函数接收应该移动到数组开头的名称作为它的参数。在清单 10-34 中,我向由Summary组件呈现的内容添加了另一个button元素,并使用onClick属性来调用新的函数 prop。

import React from "react";

export function Summary(props) {
    return (
        <React.Fragment>
            <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
            <td>
                <button className="btn btn-primary btn-sm"
                    onClick={ props.reverseCallback }>
                        Change
                </button>
                <button className="btn btn-info btn-sm m-1"
                    onClick={ () => props.promoteCallback(props.name) }>
                            Promote
                </button>
            </td>
        </React.Fragment>
    )
}

Listing 10-34Invoking a Function Prop in the Summary.js File in the src Folder

不是让App组件计算出选择了哪个名称,而是用一个参数调用函数 prop。

...
<button className="btn btn-info btn-sm m-1"
        onClick={ () => props.promoteCallback(props.name) }>
    Promote
</button>
...

onClick表达式是一个粗箭头函数,在被调用时调用函数 prop。像这样定义一个函数是很重要的,如果你只是简单地在表达式中直接指定函数 prop,你不会得到你期望的结果,正如侧栏中所描述的。点击其中一个升级按钮会将相应的名称移动到数组的第一个位置,使其显示在表格的顶部,如图 10-15 所示。

img/473159_1_En_10_Fig15_HTML.jpg

图 10-15

使用参数调用函数属性

避免过早调用的陷阱

当您需要使用参数调用函数属性时,您应该始终指定一个调用属性的粗箭头函数,如下所示:

...
<button onClick={ () => props.promoteCallback(props.name) }>
    Promote
</button>
...

您几乎肯定会忘记至少这样做一次,并在表达式中直接调用函数 prop,就像这样:

...
<button onClick={ props.promoteCallback(props.name) }>
    Promote
</button>
...

React 将在组件呈现其内容时计算表达式,这将调用 prop,即使用户没有单击 button 元素。这很少是预期的效果,可能会导致意外的行为或产生错误,这取决于调用 prop 时它做了什么。例如,在清单 10-34 中的组件的情况下,效果是创建一个“超过最大更新深度”错误,这是因为函数 prop 要求 React 重新呈现组件,这导致Summary组件呈现内容,这再次调用 prop。这一直持续到 React 停止执行并报告错误。

将属性传递给子组件

React 应用是通过组合组件创建的,创建了一系列父子关系。这种安排通常要求组件从其父组件接收数据值或回调函数,并将其传递给其子组件。为了演示属性是如何传递的,我在src文件夹中添加了一个名为CallbackButton.js的文件,并用它来定义清单 10-35 中所示的组件。

import React from "react";

export function CallbackButton(props) {
    return (
        <button className={`btn btn-${props.theme} btn-sm m-1`}
                onClick={ props.callback }>
            { props.text}
        </button>
    )
}

Listing 10-35The Contents of the CallbackButton.js File in the src Folder

这个组件呈现一个button元素,它的文本内容是使用名为text的属性设置的,当点击这个元素时,它会调用通过名为callback的属性提供的一个函数。还有一个用于为button元素选择引导 CSS 样式的theme属性。

在清单 10-36 中,我已经更新了Summary组件以使用CallbackButton组件,它通过从其父组件传递属性并添加自己的额外属性来进行配置。

import React from "react";

import { CallbackButton } from "./CallbackButton";

export function Summary(props) {
    return  (
        <React.Fragment>
            <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
            <td>
                <CallbackButton theme="primary"
                    text="Reverse" callback={ props.reverseCallback } />
                <CallbackButton theme="info" text="Promote"
                    callback={ () => props.promoteCallback(props.name)} />
            </td>
        </React.Fragment>
    )
}

Listing 10-36Adding a Component in the Summary.js File in the src Folder

接收属性的组件不知道——也不关心——它们来自哪里,它们是通过同一个props参数接收的,产生如图 10-16 所示的结果。

img/473159_1_En_10_Fig16_HTML.jpg

图 10-16

传递属性

将所有属性传递给子组件

如果组件的父级提供的属性与组件的子级所期望的属性具有相同的名称,则可以使用析构运算符。为了演示,我在src文件夹中添加了一个名为SimpleButton.js的文件,并用它来定义清单 10-37 中所示的组件。

import React from "react";

export function SimpleButton(props) {
    return (
        <button onClick={ props.callback } className={props.className}>
            { props.text}
        </button>
    )
}

Listing 10-37The Contents of the SimpleButton.js File in the src Folder

SimpleButton组件需要callbackclassNametext属性。当CallbackButton组件应用SimpleButton组件时,父组件提供的属性之间有重叠,这意味着析构操作符可以用来传递属性,如清单 10-38 所示。

import React from "react";

import { SimpleButton } from "./SimpleButton";

export function CallbackButton(props) {
    return (
        <SimpleButton {...props} className={`btn btn-${props.theme} btn-sm m-1`} />
    )
}

Listing 10-38Passing on Props in the CallbackButton.js File in the src Folder

{...props}表达式传递从父组件接收的所有属性,这些属性由className属性补充。如果一个组件想要对其子组件保留特定的属性,那么可以使用稍微不同的方法,如清单 10-39 所示。

import React from "react";
import { SimpleButton } from "./SimpleButton";

export function CallbackButton(props) {
    let { theme, ...childProps} = props;
    return (
        <SimpleButton { ...childProps }
            className={`btn btn-${props.theme} btn-sm m-1`} />
    )
}

Listing 10-39Selectively Passing on Props in the CallbackButton.js File in the src Folder

rest 操作符在一个语句中使用,该语句创建一个包含除了theme之外的所有父对象的childProps对象。析构操作符用于将属性从childProps对象传递给子组件。

提供默认的属性值

随着应用中使用的属性数量的增加,您可能会发现自己在重复相同的一组属性值,即使这些值每次都相同。另一种方法是定义一组默认值,并在需要使用不同的值时仅覆盖它们。在清单 10-40 中,我为CallbackButton组件定义了一组默认属性值。

import React from "react";
import { SimpleButton } from "./SimpleButton";

export function CallbackButton(props) {
    let { theme, ...childProps} = props;
    return (
        <SimpleButton {...childProps}
            className={`btn btn-${props.theme} btn-sm m-1`} />
    )
}

CallbackButton.defaultProps = {

    text: "Default Text",
    theme: "warning"

}

Listing 10-40Defining Default Values in the CallbackButton.js File in the src Folder

名为defaultProps的属性被添加到组件中,并被赋予一个对象,该对象为在父组件不提供值时使用的属性提供默认值。在清单 10-41 中,我修改了Summary组件,使其依赖于一个CallbackButton元素的默认属性,但为另一个元素提供值。

import React from "react";
import { CallbackButton } from "./CallbackButton";

export function Summary(props) {
    return  (
        <React.Fragment>
            <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
            <td>
                <CallbackButton callback={props.reverseCallback} />
                <CallbackButton theme="info" text="Promote"
                    callback={ () => props.promoteCallback(props.name)} />
            </td>
        </React.Fragment>
    )
}

Listing 10-41Relying on Prop Defaults in the Summary.js File in the src Folder

第一个CallbackButton元素依赖默认值,产生如图 10-17 所示的结果。

img/473159_1_En_10_Fig17_HTML.jpg

图 10-17

使用默认属性值

类型检查属性值

prop 不能指示它们期望接收什么数据类型,并且当它们不能使用作为 prop 接收的数据值时,没有办法通知它们的祖先组件。为了帮助避免这些问题,React 允许一个组件声明它所期望的属性类型,如清单 10-42 所示。

import React from "react";

import PropTypes from "prop-types";

export function SimpleButton(props) {
    return (
        <button onClick={ props.callback } className={props.className}>
            { props.text}
        </button>
    )
}

SimpleButton.defaultProps = {

    disabled: false

}

SimpleButton.propTypes = {

    text: PropTypes.string,
    theme: PropTypes.string,
    callback: PropTypes.func,
    disabled: PropTypes.bool

}

Listing 10-42Declaring Prop Types in the SimpleButton.js File in the src Folder

一个propTypes属性被添加到组件中,并被赋予一个对象,该对象的属性名对应于属性名,其值指定组件期望的类型。使用从prop-types包中导入的PropTypes值指定类型,表 10-3 中描述了最有用的PropTypes值。

表 10-3

有用的 PropTypes 值

|

名字

|

描述

| | --- | --- | | array | 这个值指定一个属性应该是一个数组。 | | bool | 这个值指定了一个属性应该是布尔值。 | | func | 该值指定一个属性应该是一个函数。 | | number | 该值指定属性应该是一个数字值。 | | object | 该值指定属性应该是一个对象。 | | string | 该值指定属性应该是字符串。 |

小费

您可以将表 10-3 中的任何类型与isRequired组合,以便在父组件PropTypes.bool.isRequired没有提供该属性的值时生成警告。

为了演示如何检查类型,在清单 10-43 中,我为disabled属性的CallbackButton元素添加了一个值,使用了一个字符串值,而不是清单 10-42 中指定的bool

import React from "react";
import { CallbackButton } from "./CallbackButton";

export function Summary(props) {
    return  (
        <React.Fragment>
            <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
            <td>
                <CallbackButton callback={props.reverseCallback} />
                <CallbackButton theme="info" text="Promote"
                    callback={ () => props.promoteCallback(props.name)}
                    disabled="true" />
            </td>
        </React.Fragment>
    )
}

Listing 10-43Providing the Wrong Type in the Summary.js File in the src Folder

这是一个常见的错误,在应该是boolnumber的地方使用了字符串值。很难找出问题出在哪里,尤其是因为 prop 是由出现问题的组件的祖先定义的。使用 prop 类型使问题变得明显。当您保存更改时,浏览器将重新加载,您将看到浏览器的 JavaScript 控制台中显示以下消息:

...
index.js:2178 Warning: Failed prop type: Invalid prop `disabled` of type `string` supplied to `SimpleButton`, expected `boolean`.
...

为了解决这个问题,我可以更改 prop 值,以便它将预期的类型发送给组件。另一种方法是使组件更加灵活,以便它能够处理disabled属性的Booleanstring值。考虑到在需要Boolean值时创建string属性值是多么常见,这是一个好主意,尤其是如果您正在编写将被其他开发团队使用的组件。在清单 10-44 中,我添加了对SimpleButton组件的支持来处理这两种类型,并更新了它的propTypes配置来反映这一变化。

注意

prop 类型检查仅在开发过程中执行,当应用准备好进行部署时将被禁用。请参见第八章,了解准备应用进行部署的示例。

import React from "react";
import PropTypes from "prop-types";

export function SimpleButton(props) {
    return (
        <button onClick={ props.callback } className={props.className}
                disabled={ props.disabled === "true" || props.disabled === true }>
            { props.text}
        </button>
    )
}

SimpleButton.defaultProps = {
    disabled: false
}

SimpleButton.propTypes = {
    text: PropTypes.string,
    theme: PropTypes.string,
    callback: PropTypes.func,
    disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.string])
}

Listing 10-44Accepting Multiple Prop Types in the SimpleButton.js File in the src Folder

有两种有用的PropTypes方法可用于指定多种类型或特定值,如表 10-4 所述。

表 10-4

有用的 PropTypes 方法

|

名字

|

描述

| | --- | --- | | oneOfType | 该方法接受组件愿意接收的一组PropTypes值。 | | oneOf | 该方法接受组件愿意接收的一组值。 |

在清单 10-44 中,我使用oneOfType方法告诉 React】属性可以接受Booleanstring值。组件能够处理清单 10-43 中为disabled属性提供的值 I,这将禁用button元素,如图 10-18 所示。

img/473159_1_En_10_Fig18_HTML.jpg

图 10-18

接受多种属性类型

小费

另一种方法是在应用组件时将 prop 值改为一个Boolean,这可以通过使用disabled属性的表达式来完成:disabled={ true }

摘要

在本章中,我介绍了无状态组件,它是 React 应用中关键构件的最简单版本。我演示了如何定义无状态组件,如何呈现内容,以及如何组合组件来创建更复杂的功能。我还解释了父组件如何使用 props 将数据传递给其子组件,并向您展示了 props 如何用于函数,这提供了组件间通信所需的基本特性。我已经向你展示了定义默认值和属性类型的特性,从而结束了这一章。在下一章,我将解释如何创建有状态数据的组件。

十一、有状态组件

在这一章中,我将介绍有状态组件,它建立在第十章中描述的特性之上,并添加了每个组件独有的状态数据,这些数据可用于改变呈现的输出。表 11-1 将有状态组件放在上下文中。

表 11-1

将有状态组件放在上下文中

|

问题

|

回答

| | --- | --- | | 它们是什么? | 组件是 React 应用中的关键构件。有状态组件有自己的数据,可以用来改变组件呈现的内容。 | | 它们为什么有用? | 有状态组件使跟踪每个组件提供的应用状态变得更加容易,并提供了改变数据值和反映呈现给用户的内容变化的方法。 | | 它们是如何使用的? | 有状态组件是使用类或通过向功能组件添加挂钩来定义的。 | | 有什么陷阱或限制吗? | 必须注意确保状态数据被正确修改,如本章“修改状态数据”一节所述。 | | 有其他选择吗? | 组件是 React 应用中的关键构建块,没有办法避免使用它们。在更大更复杂的项目中,有一些替代属性是有用的,如后面章节所述。 |

表 11-2 总结了本章内容。

表 11-2

章节总结

|

问题

|

解决办法

|

列表

| | --- | --- | --- | | 向组件添加状态数据 | 定义一个类,其构造函数设置状态属性或调用useState函数为单个状态属性创建属性和函数 | 4–5, 12, 13 | | 修改状态数据 | 调用setState函数或调用useState返回的函数 | 6–11 | | 在组件之间共享数据 | 将状态数据提升到祖先组件,并使用 props 分发它 | 14–18 | | 在基于类组件中定义适当的类型和默认值 | 将属性应用于该类或在该类中定义静态属性 | 19–20 |

为本章做准备

在本章中,我继续使用在第十章中创建的components项目。为了准备这一章,我修改了由Summary组件呈现的内容,使其直接使用SimpleButton组件,如清单 11-1 所示,而不是我用来描述属性如何分发的CallbackButton

小费

你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。

import React from "react";

//import { CallbackButton } from "./CallbackButton";

import { SimpleButton } from "./SimpleButton";

export function Summary(props) {
    return (
        <React.Fragment>
            <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
            <td>
                <SimpleButton
                    className="btn btn-warning btn-sm m-1"
                    callback={ props.reverseCallback }
                    text={ `Reverse (${ props.name })`}
                />
                <SimpleButton
                    className="btn btn-info btn-sm m-1"
                    callback={ () => props.promoteCallback(props.name)}
                    text={ `Promote (${ props.name})`}
                />
            </td>
        </React.Fragment>
    )
}

Listing 11-1Changing the Content in the Summary.js File in the src Folder

在清单 11-2 中,我已经删除了SimpleButton组件的属性的类型和默认值,我将在本章末尾恢复这些属性。

import React from "react";

export function SimpleButton(props) {
    return (
        <button onClick={ props.callback } className={props.className}
                disabled={ props.disabled === "true" || props.disabled === true }>
            { props.text}
        </button>
    )
}

Listing 11-2Removing Properties in the SimpleButton.js File in the src Folder

打开命令提示符,导航到components文件夹,运行清单 11-3 中所示的命令来启动 React 开发工具。

npm start

Listing 11-3Starting the Development Tools

在初始构建过程之后,一个新的浏览器窗口将会打开并显示如图 11-1 所示的内容。

img/473159_1_En_11_Fig1_HTML.jpg

图 11-1

运行示例应用

了解不同的组件类型

在接下来的小节中,我将解释 React 支持的组件类型之间的区别。当您看到有状态组件与第十章中描述的无状态组件的主要区别时,理解有状态组件如何工作将会更容易。

理解无状态组件

正如你在第十章中看到的,无状态组件由一个函数组成,React 调用该函数来响应定制的 HTML 元素,并将正确的值作为参数传递。自定义 HTML 元素上相同的一组属性值将导致相同的属性参数并产生相同的结果,如图 11-2 所示。

img/473159_1_En_11_Fig2_HTML.jpg

图 11-2

无状态组件的可预测结果

一个无状态组件将总是呈现相同的 HTML 元素,给定相同的一组属性值,不管函数被调用的频率如何。它完全依赖父组件提供的属性值来呈现其内容。这意味着无论应用中有多少个SimpleButton元素,React 都可以继续调用同一个函数,并且只需跟踪哪些属性与每个SimpleButton元素相关联。

了解有状态组件

一个有状态组件有自己的数据,这些数据影响组件呈现的内容。这个数据被称为状态数据,与父组件和它提供的属性是分开的。

想象一下,SimpleButton组件必须记录用户点击它所呈现的button元素的次数,并将当前次数显示为元素的内容。为了提供这一功能,组件需要一个计数器,每次单击按钮时该计数器都会递增,并且在呈现其内容时必须包含计数器的当前值。

由父组件定义的每个SimpleButton元素将产生一个button元素,需要一个单独的计数器,因为每个按钮可以独立于其他按钮被点击。有状态组件是 JavaScript 对象,应用组件的SimpleButton HTML 元素和组件对象之间是一一对应的关系,每个组件都有自己的状态,可能呈现不同的输出,如图 11-3 所示。

img/473159_1_En_11_Fig3_HTML.jpg

图 11-3

带计数器的有状态组件

向有状态组件提供相同的属性将会产生相同的结果,这不再是确定无疑的了,因为每个组件对象的状态数据可以有不同的值,并使用它来生成不同的结果。

正如您将了解到的,有状态组件有许多无状态组件所没有的特性,如果您记得每个有状态组件都是一个 JavaScript 对象,有自己的状态数据,并且与一个单独的定制 HTML 元素相关联,您会发现这些特性更容易理解。

创建有状态组件

首先,我将把示例应用中的一个现有的SimpleButton组件从无状态组件转换为有状态组件,这将让我在转到更复杂的特性之前解释一些基础知识。

定义一个有状态的组件是使用一个类来完成的,这个类是一个描述每个组件对象将拥有的功能的模板,如第四章所述。在清单 11-4 中,我用一个类替换了SimpleButton组件的功能。

注意

这是一个没有任何状态数据的有状态组件。我将解释如何定义组件,然后在“添加状态数据”一节中向您展示如何添加状态数据。

import React, { Component } from "react";

export class SimpleButton extends Component {

    render() {
        return (
            <button onClick={ this.props.callback }
                    className={ this.props.className }
                    disabled={ this.props.disabled === "true"
                        || this.props.disabled === true  }>
                { this.props.text}
            </button>
        )
    }
}

Listing 11-4Introducing a Class in the SimpleButton.js File in the src Folder

在接下来的部分中,我将描述清单 11-4 中的每一个变化,并解释它们是如何被用来创建有状态组件的。

了解组件类

当您定义一个有状态组件时,您使用classextends关键字来表示一个类,该类继承了在react包中定义的Component类所提供的功能,就像这样:

...
export class SimpleButton extends Component {
...

这种关键字组合定义了一个名为SimpleButton的类,它扩展了 React 提供的Component类。export关键字使得SimpleButton类可以在定义它的 JavaScript 文件之外使用,就像组件被定义为函数时一样。

理解导入语句

为了从Component类扩展,使用了一个import,如下所示:

...
import React, { Component } from "react";
...

正如我在第四章中解释的,这个语句中有两种类型的导入。从react包的默认导出被导入并被赋予名称React,这允许 JSX 工作。react包还有一个名为Component的导出,它是用花括号({}字符)导入的。创建有状态组件时,严格按照所示使用import语句是很重要的。

了解渲染方法

有状态组件的主要目的是呈现内容以供显示。不同之处在于,这是在一个名为render的方法中完成的,当 React 希望组件进行渲染时会调用该方法。render方法必须返回一个 React 元素,该元素可以使用React.createElement方法创建,或者更常见的是,作为 HTML 的一个片段。

...

render() {

    return (
        <button onClick={ this.props.callback }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                    || this.props.disabled === true  }>
            { this.props.text}
        </button>
    )
}
...

了解有状态组件属性

当您开始使用有状态组件时,最明显的区别之一是您必须使用this关键字来访问属性值,如下所示:

...
return (
    <button onClick={ this.props.callback }
            className={ this.props.className }
            disabled={ this.props.disabled === "true"
                || this.props.disabled === true  }>
        { this.props.text}
    </button>
)
...

this关键字指的是组件的 JavaScript 对象。当使用有状态组件时,您必须使用this关键字来访问props属性,如果您忘记了:

./src/SimpleButton.js  Line 7:  'props' is not defined  no-undef

虽然我重新定义了组件,但我没有改变它呈现的内容或改变它的行为方式,结果和组件被定义为函数时一样,如图 11-4 所示。

img/473159_1_En_11_Fig4_HTML.jpg

图 11-4

引入有状态组件

添加状态数据

有状态组件最重要的特性是组件的每个实例都有自己的数据,称为状态数据。在清单 11-5 中,我向SimpleButton组件添加了状态数据。

import React, { Component } from "react";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.props.callback }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.state.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }
}

Listing 11-5Adding State Data in the SimpleButton.js File in the src Folder

状态数据是使用一个构造函数定义的,这是一个特殊的方法,当使用类创建一个新对象时调用,并且必须遵循清单中所示的形式:构造函数应该定义一个props参数,第一条语句应该是使用props对象作为参数调用特殊的super方法,该方法调用Component类的构造函数并设置有状态组件中可用的特性。

一旦调用了super,就可以定义状态数据,这是通过将一个对象分配给this.state来完成的。

...
constructor(props) {
    super(props);
    this.state = {
        counter: 0,
        hasButtonBeenClicked: false
    }
}
...

状态数据被定义为对象的属性。本例中有一个属性,它创建了名为counter的状态数据属性,其值为0hasButtonBeenClicked,其值为false

读取状态数据

访问状态数据是通过读取您通过this.state定义的属性来完成的,类似于访问 props 的方式。

...
render() {
    return (
        <button onClick={ this.props.callback }
            className={ this.props.className }
            disabled={ this.props.disabled === "true"
                        || this.props.disabled === true  }>
                { this.props.text} { this.state.counter }
                { this.state.hasButtonBeenClicked &&
                    <div>Button Clicked!</div>
                }
        </button>
    )
}
...

清单 11-5 中的render方法设置button元素的内容,使其包含一个属性值和counter状态数据属性的值,产生如图 11-5 所示的效果。我在清单 11-5 中定义的额外的div元素将不会显示,直到hasButtonBeenClicked属性的值为true,我将在下一节演示。

img/473159_1_En_11_Fig5_HTML.jpg

图 11-5

定义和读取状态数据

修改状态数据

只有当状态数据可以被修改时,使用状态数据才有意义,因为这允许组件对象呈现不同的内容。React 需要一种特定的技术来修改状态数据,如清单 11-6 所示。

import React, { Component } from "react";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.handleClick }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.state.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }

    handleClick = () => {
        this.setState({
            counter: this.state.counter + 1,
            hasButtonBeenClicked: true
        });
        this.props.callback();
    }
}

Listing 11-6Modifying State Data in the SimpleButton.js File in the src Folder

React 不允许直接修改状态数据,如果您试图直接为状态属性赋值,它会报告一个错误。相反,修改是通过继承自Component类的setState方法进行的。在清单中,我添加了一个名为handleClick的方法,它由button元素的onClick表达式选择,并使用setState方法增加counter状态属性。

小费

onClick属性选择的方法必须以特定的方式定义。我将在第十二章中解释如何使用onClick属性以及如何定义它的方法。

setState方法的参数是一个对象,其属性指定要更新的状态数据,如下所示:

...
this.setState({
    counter: this.state.counter + 1,
    hasButtonBeenClicked: true
});
...

这个语句告诉 React 应该通过增加当前值来修改counter属性,并且hasButtonBeenClicked属性应该是true。注意,我没有对counter使用增量操作符(++),因为那样会给属性分配一个新值,导致错误。

小费

使用setState方法时,您只需为您想要更改的值定义属性。React 会将您指定的更改与组件状态数据的其余部分合并,并保持任何未提供值的属性不变。

虽然使用setState方法可能会感到尴尬,但好处是 React 负责重新呈现应用以反映更改的影响,这意味着我不必像在第十一章中那样手动调用ReactDOM.render方法。效果是点击按钮增加相关组件的计数器状态数据,如图 11-6 所示。(单击按钮会对表格中的行进行重新排序,这意味着您单击的按钮可能会移动到新的位置。)

img/473159_1_En_11_Fig6_HTML.jpg

图 11-6

修改状态数据

单击按钮会更改其中一个组件对象的状态,而保持其他五个组件对象不变。

避免状态数据修改陷阱

React 异步执行对状态数据的更改,并可能选择将几个更新组合在一起以提高性能,这意味着调用setState的效果可能不会以您预期的方式生效。在更新状态数据时有一些常见的陷阱,我将在接下来的小节中描述这些陷阱,以及如何避免它们的细节。

小费

React Devtools 浏览器扩展向您显示有状态组件的状态数据,这对于查看应用如何响应更改以及当您没有获得预期的行为时跟踪问题非常有用。

避免依赖值陷阱

状态数据值通常是相关的,一个常见的问题是假设每个变化的效果是单独应用的,如清单 11-7 所示。

import React, { Component } from "react";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.handleClick }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.state.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }

    handleClick = () => {
        this.setState({
            counter: this.state.counter + 1,
            hasButtonBeenClicked: this.state.counter > 0
        });
        this.props.callback();
    }
}

Listing 11-7Performing Related State Changes in the SimpleButton.js File in the src Folder

hasButtonBeenClicked属性的更新假定counter属性在其表达式被求值之前已经被改变。React 不会单独应用更改,并且使用当前的counter值来计算hasButtonBeenClicked属性的表达式。当使用对setState方法的单独调用来执行相关更新时,也会出现这个问题,如清单 11-8 所示。

import React, { Component } from "react";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.handleClick }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.state.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }

    handleClick = () => {
        this.setState({ counter: this.state.counter + 1 });
        this.setState({ hasButtonBeenClicked: this.state.counter > 0 });
        this.props.callback();
    }
}

Listing 11-8Making Dependent Updates in the SimpleButton.js File in the src Folder

为了提高效率,React 会将这些更新批处理在一起,这将产生与清单 11-6 相同的结果,并且意味着在按钮被点击两次之前hasButtonBeenClicked属性不会是true,如图 11-7 所示。

img/473159_1_En_11_Fig7_HTML.jpg

图 11-7

依赖值陷阱

当您要进行一系列相关的更改时,您可以将一个函数传递给setState方法,该方法将在状态数据更新后被调用,并可用于执行依赖于已更改的状态值的任务,如清单 11-9 所示。

import React, { Component } from "react";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.handleClick }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.state.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }

    handleClick = () => {
        this.setState({ counter: this.state.counter + 1 },
            () => this.setState({ hasButtonBeenClicked: this.state.counter > 0 }));
        this.props.callback();
    }
}

Listing 11-9Using a Callback in the SimpleButton.js File in the src Folder

使用回调函数可以确保在应用新的counter属性之前hasButtonBeenClicked的值不会改变,从而确保这些值是同步的,如图 11-8 所示。

img/473159_1_En_11_Fig8_HTML.jpg

图 11-8

强制状态改变按顺序执行

避免遗漏更新陷阱

React 应用更新的方式意味着对同一状态数据属性的多次更改将被忽略,只有最近的值被应用,如清单 11-10 所示。

...
handleClick = () => {
    for (let i = 0; i < 5; i++) {
        this.setState({ counter: this.state.counter + 1});
    }
    this.setState({ hasButtonBeenClicked: true });
    this.props.callback();
}
...

Listing 11-10Making Multiple Updates in the SimpleButton.js File in the src Folder

在实际项目中,多次更新通常是在处理数据时进行的,而不是在一个for循环中进行,例如,对数组中的每个对象执行一次状态更改。这个清单展示了重复修改相同属性的效果:点击一个按钮将值增加 1,而不是增加 5 次counter值,如图 11-9 所示。

img/473159_1_En_11_Fig9_HTML.jpg

图 11-9

对状态属性应用多个更新

如果您需要执行多次更新并让每次更新按顺序生效,那么您可以使用接受一个函数作为第一个参数的setState方法版本。该函数提供了当前状态数据和一个 props 对象,如清单 11-11 所示。

小费

这个版本的setState方法对于更新嵌套状态属性也很有用,你可以在第十四章中看到演示。

...
handleClick = () => {
    for (let i = 0; i < 5; i++) {
        this.setState((state, props) => { return { counter: state.counter + 1 }});
    }
    this.setState({ hasButtonBeenClicked: true });
    this.props.callback();
}
...

Listing 11-11Making Multiple Updates in the SimpleButton.js File in the src Folder

传递给setState方法的函数使用与前面示例相同的格式返回一个更新对象。不同之处在于,状态数据对象反映了所有以前的变化,这些变化被组合在一起并可用于重复更新,产生如图 11-10 所示的效果。

img/473159_1_En_11_Fig10_HTML.jpg

图 11-10

对状态属性应用多个更新

使用钩子定义有状态组件

并不是所有的开发人员都喜欢使用类来定义有状态组件,所以 React 提供了一种替代方法,叫做钩子,它允许功能组件定义状态数据。在清单 11-12 中,我在src文件夹中添加了一个名为HooksButton.js的文件,并重新创建了清单 11-11 中的有状态组件,作为一个使用钩子的函数。

import React, { useState } from "react";

export function HooksButton(props) {
    const [counter, setCounter] = useState(0);
    const [hasButtonBeenClicked, setHasButtonBeenClicked] = useState(false);

    const handleClick = () => {
        setCounter(counter + 5);
        setHasButtonBeenClicked(true);
        props.callback();
    }

    return (
        <button onClick={ handleClick }
            className={ props.className }
            disabled={ props.disabled === "true" || props.disabled === true  }>
                { props.text} { counter }
                { hasButtonBeenClicked && <div>Button Clicked!</div>}
        </button>
    )
}

Listing 11-12The Contents of the HooksButton.js File in the src Folder

useState函数用于创建状态数据。它的参数是 state data 属性的初始值,它返回一个提供当前值的属性和一个更改值并触发更新的函数。属性和函数以数组形式返回,并使用数组析构为其分配有意义的名称,如下所示:

...
const [counter, setCounter] = useState(0);
...

该语句创建了一个名为counter的状态数据属性,其初始值为零,其值可以使用名为setCounter的函数进行更改。用于更改状态数据属性值的函数不具备setState方法的所有特性,这就是为什么我在handleClick函数中将值增加了 5,而不是执行一系列单独的更新,如清单 11-11 所示。

...
const handleClick = () => {
    setCounter(counter + 5);
    setHasButtonBeenClicked(true);
    props.callback();
}
...

在清单 11-13 中,我已经更新了Summary,所以它使用了HooksButton组件。

import React from "react";
import { SimpleButton } from "./SimpleButton";
import { HooksButton } from "./HooksButton";

export function Summary(props) {
    return (
        <React.Fragment>
            <td>{ props.index + 1} </td>
            <td>{ props.name } </td>
            <td>{ props.name.length } </td>
            <td>
                <SimpleButton
                    className="btn btn-warning btn-sm m-1"
                    callback={ props.reverseCallback }
                    text={ `Reverse (${ props.name })`} />
                <HooksButton
                    className="btn btn-info btn-sm m-1"
                    callback={ () => props.promoteCallback(props.name)}
                    text={ `Promote (${ props.name})`} />
            </td>
        </React.Fragment>
    )
}

Listing 11-13Using the Hooks Component in the Summary.js File in the src Folder

钩子的使用对于Summary组件是不可见的,它像往常一样通过 props 提供数据和函数。这个例子产生了相同的结果,如图 11-10 所示。

你应该使用钩子还是类?

钩子为不喜欢使用类的开发人员提供了另一种创建有状态组件的方法。根据您的个人偏好,要么这将是适合您的编码风格的重要特性,要么您将继续定义类并完全忘记钩子。

React 的未来版本将支持钩子和类特性,因此您可以使用最适合自己的特性,也可以随意混合搭配。我喜欢钩子的特性,但是,除了在第十三章描述一些相关的钩子特性之外,本书中的所有例子都使用了类。这部分是因为 hooks 特性是新的——但也是因为我已经使用基于类的编程语言很长时间了,使用类来定义组件符合我思考代码的方式,即使是简单的无状态组件。

如果你更喜欢使用钩子,但是不使用类就无法表达书中的例子,那么给我发电子邮件到 adam@adam-freeman.com,我会试着给你指出正确的方向。

提升状态数据

目前,每个SimpleButtonHooksButton组件都是独立存在的,并且有自己的状态数据,所以单击一个按钮只会影响单个组件的状态值,而不会影响其他组件。

当组件需要访问相同的数据时,需要不同的方法。在这种情况下,状态数据被提升,这意味着它被移动到第一个公共祖先组件,并使用 props 分发回需要它的组件。

小费

在 React 组件之间共享数据还有其他方法。第十三章描述了上下文特性,更复杂的项目可以受益于使用数据存储(参见第 19 和 20 章)或 URL 路由(参见第 21 和 22 章)。

例如,如果我希望同一个表行中的SimpleButtonHooksButton组件共享一个counter值,我需要在第一个公共祖先中定义 state data 属性,这就是Summary组件。在清单 11-14 中,我将Summary转换成了一个基于类的有状态组件,它定义了一个计数器值。

import React, { Component } from "react";

import { SimpleButton } from "./SimpleButton";
import { HooksButton } from "./HooksButton";

export class Summary extends Component {

    constructor(props) {
        super(props);
        this.state = {
            counter: 0
        }
    }

    incrementCounter = (increment) => {
        this.setState((state) => { return { counter: state.counter + increment}});
    }

    render() {
        const props = this.props;
        return (
            <React.Fragment>
                <td>{ props.index + 1} </td>
                <td>{ props.name } </td>
                <td>{ props.name.length } </td>
                <td>
                    <SimpleButton
                        className="btn btn-warning btn-sm m-1"
                        callback={ props.reverseCallback }
                        text={ `Reverse (${ props.name })`}
                        counter={ this.state.counter }
                        incrementCallback={this.incrementCounter }
                    />
                    <HooksButton
                        className="btn btn-info btn-sm m-1"
                        callback={ () => props.promoteCallback(props.name)}
                        text={ `Promote (${ props.name})`}
                        counter={ this.state.counter }
                        incrementCallback={this.incrementCounter }
                    />
                </td>
            </React.Fragment>
        )
    }
}

Listing 11-14Lifting Up State Data in the Summary.js File in the src Folder

Summary组件定义了一个counter属性,并将其作为属性传递给其子组件。该组件还定义了一个incrementCounter方法,子组件将调用该方法来更改counter属性,该属性是使用一个名为incrementCallback的属性传递的。这是必需的,不仅因为状态数据不是直接修改的,而且因为属性是只读的。incrementCounter方法使用带有函数的setState方法,这样它可以被子组件重复调用。

小费

我在render方法中定义了一个props属性,这样我就不必为了使用this关键字而改变所有的引用,这在转换函数组件以使用类时是一个有用的快捷方式。

在清单 11-15 中,我从SimpleButton组件中移除了counter状态数据属性,并使用了counterincrementCounter属性。

import React, { Component } from "react";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            // counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.handleClick }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.props.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }

    handleClick = () => {
        this.props.incrementCallback(5);
        this.setState({ hasButtonBeenClicked: true });
        this.props.callback();
    }
}

Listing 11-15Replacing State Data with Props in the SimpleButton.js File in the src Folder

需要对HooksButton组件进行相应的修改,这些组件将共享相同的属性集,如清单 11-16 所示。

import React, { useState } from "react";

export function HooksButton(props) {
    //const [counter, setCounter] = useState(0);
    const[ hasButtonBeenClicked, setHasButtonBeenClicked] = useState(false);

    const handleClick = () => {
        //setCounter(counter + 5);
        props.incrementCallback(5);
        setHasButtonBeenClicked(true);
        props.callback();
    }

    return (
        <button onClick={ handleClick }
            className={ props.className }
            disabled={ props.disabled === "true" || props.disabled === true  }>
                { props.text} { props.counter }
                { hasButtonBeenClicked && <div>Button Clicked!</div>}
        </button>
    )
}

Listing 11-16Replacing State Data with Props in the HooksButton.js File in the src Folder

counter状态属性提升到父组件意味着每一行表格中呈现给用户的两个按钮共享其父组件的状态数据,这样点击其中一个按钮元素就会导致两个按钮都被更新,如图 11-11 所示。

img/473159_1_En_11_Fig11_HTML.jpg

图 11-11

提升状态数据

不是每一项状态数据都必须被提升,并且单独的组件仍然有它们自己的本地状态数据,因此hasButtonBeenClicked属性保持在本地并且独立于其他组件。

进一步提升状态数据

状态数据可以比父组件提升得更远。如果我想让所有的SimpleButtonHooksButton组件共享同一个counter属性,那么我可以把它提升到App组件,如清单 11-17 所示,其中我使用钩子特性创建了有状态。

import React, { useState } from "react";

import { Summary } from "./Summary";
import ReactDOM from "react-dom";

let names = ["Bob", "Alice", "Dora"]

function reverseNames() {
    names.reverse();
    ReactDOM.render(<App />, document.getElementById('root'));
}

function promoteName(name) {
    names = [name, ...names.filter(val => val !== name)];
    ReactDOM.render(<App />, document.getElementById('root'));
}

export default function App() {
    const [counter, setCounter] = useState(0);

    const incrementCounter = (increment) => setCounter(counter + increment);

    return (
        <table className="table table-sm table-striped">
            <thead>
                <tr><th>#</th><th>Name</th><th>Letters</th></tr>
            </thead>
            <tbody>
                { names.map((name, index) =>
                    <tr key={ name }>
                        <Summary index={index} name={name}
                            reverseCallback={reverseNames}
                            promoteCallback={promoteName}
                            counter={ counter }
                            incrementCallback={ incrementCounter }
                        />
                    </tr>
                )}
            </tbody>
        </table>
    )
}

Listing 11-17Lifting State Data in the App.js File in the src Folder

App组件定义了counter状态属性和通过调用setCounter函数修改它的incrementCounter方法。在清单 11-18 中,我已经从Summary组件中移除了状态数据,并将从App组件接收的属性传递给子组件。

import React, { Component } from "react";
import { SimpleButton } from "./SimpleButton";
import { HooksButton } from "./HooksButton";

export class Summary extends Component {

    // constructor(props) {
    //     super(props);
    //     this.state = {
    //         counter: 0
    //     }
    // }

    // incrementCounter = (increment) => {
    //     this.setState((state) => { return { counter: state.counter + increment}});
    // }

    render() {
        const props = this.props;
        return (
            <React.Fragment>
                <td>{ props.index + 1} </td>
                <td>{ props.name } </td>
                <td>{ props.name.length } </td>
                <td>
                    <SimpleButton
                        className="btn btn-warning btn-sm m-1"
                        callback={ props.reverseCallback }
                        text={ `Reverse (${ props.name })`}
                        { ...this.props }
                    />
                    <HooksButton
                        className="btn btn-info btn-sm m-1"
                        callback={ () => props.promoteCallback(props.name)}
                        text={ `Promote (${ props.name})`}
                        { ...this.props }
                    />
                </td>
            </React.Fragment>
        )
    }
}

Listing 11-18Removing State Data in the Summary.js File in the src Folder

当有状态组件没有状态数据时,不需要构造函数,如果您定义了一个构造函数,它除了使用super将属性传递给基类之外什么也不做,您将会收到一个警告。我使用析构操作符将从App组件收到的属性传递给SimpleButtonHooksButton组件。

现在状态数据已经提升到了App组件,所有作为App组件的后代的SimpleButton组件共享一个counter值,如图 11-12 所示。

img/473159_1_En_11_Fig12_HTML.jpg

图 11-12

将状态数据提升到顶级组件

不需要对SimpleButtonHooksButton组件进行任何更改,它们不知道状态数据是在哪里定义的,并且接收数据值和作为属性更改数据值所需的回调函数。

定义属性类型和默认值

在这一章的开始,我删除了适当的默认值和类型,这样我就可以专注于从无状态到有状态组件的转换。基于类的组件以与功能组件相同的方式支持这些特性,如清单 11-19 所示。

import React, { Component } from "react";

import PropTypes from "prop-types";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            // counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.handleClick }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.props.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }

    handleClick = () => {
        this.props.incrementCallback(5);
        this.setState({ hasButtonBeenClicked: true });
        this.props.callback();
    }

}

SimpleButton.defaultProps = {

    disabled: false

}

SimpleButton.propTypes = {

    text: PropTypes.string,
    theme: PropTypes.string,
    callback: PropTypes.func,
    disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.string ])

}

Listing 11-19Adding Prop Types and Values in the SimpleButton.js File in the src Folder

您还可以使用用关键字static修饰的类属性来定义类型和默认属性值,如清单 11-20 所示。static关键字定义了一个属性,该属性应用于组件的类,而不是从该类创建的对象,并由构建过程转换成清单 11-19 中使用的相同形式。

import React, { Component } from "react";
import PropTypes from "prop-types";

export class SimpleButton extends Component {

    constructor(props) {
        super(props);
        this.state = {
            // counter: 0,
            hasButtonBeenClicked: false
        }
    }

    render() {
        return (
            <button onClick={ this.handleClick }
                className={ this.props.className }
                disabled={ this.props.disabled === "true"
                            || this.props.disabled === true  }>
                    { this.props.text} { this.props.counter }
                    { this.state.hasButtonBeenClicked &&
                        <div>Button Clicked!</div>
                    }
            </button>
        )
    }

    handleClick = () => {
        this.props.incrementCallback(5);
        this.setState({ hasButtonBeenClicked: true });
        this.props.callback();
    }

    static defaultProps = {
        disabled: false
    }

    static propTypes = {
        text: PropTypes.string,
        theme: PropTypes.string,
        callback: PropTypes.func,
        disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.string ])
    }
}

Listing 11-20Defining Static Properties in the SimpleButton.js File in the src Folder

这些改变并没有改变示例应用的外观,但是它们确保了组件只接收它所期望的属性类型,并且有一个默认的属性值disabled

摘要

在这一章中,我介绍了有状态组件,它有自己的数据值,可以用来改变呈现的输出。我解释了有状态组件是使用类定义的,并向您展示了如何在构造函数中定义状态数据。我还向您展示了修改状态数据的不同方式,以及如何避免最常见的陷阱。在下一章,我将解释 React 如何处理事件。