React-和-ReactNative-第五版-一-

69 阅读20分钟

React 和 ReactNative 第五版(一)

原文:zh.annas-archive.org/md5/47e218557a614bce0d999181bbb2b76b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

几年来,ReactReact Native在 JavaScript 开发者中已被证明是全面且实用的 React 生态系统指南的热门选择。第五版包含了 React 的最新功能、增强和修复,同时与 React Native 兼容。它包括新的章节,涵盖了使用 React 和TypeScript进行现代跨平台应用开发的关键特性和概念。

从 React 的基础知识到流行的特性,如Hooks、服务器端渲染和单元测试,这本权威指南将按步骤帮助你成为一名专业的 React 开发者。

你将从学习 React 组件的基本构建块开始。接下来,你将学习如何使用 TypeScript 提高组件的稳定性。随着你通过章节的深入,你将在应用开发中使用更高级的功能,然后通过为 Web 和原生平台开发用户界面组件来应用你的知识。

在本书结束时,你将能够自信地构建 Web 上的 React 应用和多个平台上的 React Native 应用:Web、移动和桌面。

这本书面向谁

这本书适合任何想要开始学习如何使用 React 和 React Native 进行移动和 Web 应用开发的 JavaScript 开发者。不需要具备 React 的先验知识;然而,为了能够跟上本书涵盖的内容,需要具备 JavaScript、HTML 和 CSS 的实际操作能力。

这本书涵盖的内容

第一章为什么选择 React?,描述了 React 是什么以及为什么你想用它来构建你的应用。

第二章使用 JSX 进行渲染,介绍了 React 组件使用的标记语言 JSX 的基础知识。

第三章理解 React 组件和 Hooks,介绍了 React 应用中组件和 Hooks 的核心机制。

第四章React 方式的事件处理,概述了 React 组件如何处理事件。

第五章构建可重用组件,通过示例指导你进行组件重构的过程。

第六章使用 TypeScript 进行类型检查和验证,描述了 React 组件经历的各个阶段以及为什么这对 React 开发者来说很重要。

第七章使用 Routes 处理导航,提供了大量如何为你的 React Web 应用设置路由的示例。

第八章使用懒加载组件和 Suspense 进行代码拆分,介绍了导致性能更优、更高效的应用的代码拆分技术。

第九章用户界面框架组件,概述了如何开始使用 MUI,这是一个用于构建 UI 的 React 组件库。

第十章高性能状态更新,深入探讨了 React 中允许进行高效状态更新和高性能应用的新特性。

第十一章,从服务器获取数据,讨论了我们可以使用各种方式从服务器检索数据。

第十二章,React 中的状态管理,涵盖了使用 Redux 和 Mobx 等流行解决方案在应用中管理状态。

第十三章,服务器端渲染,教你如何使用 Next.js 构建在服务器和客户端上渲染内容的庞大 React 应用程序。

第十四章,React 中的单元测试,概述了使用 Vitest 进行单元测试的软件测试。

第十五章,为什么选择 React Native?,描述了 React Native 库是什么以及与原生移动开发的区别。

第十六章,React Native 内部机制,概述了 React Native 的架构。

第十七章,启动 React Native 项目,教你如何开始一个新的 React Native 项目。

第十八章,使用 Flexbox 构建响应式布局,描述了如何创建布局并添加样式。

第十九章,屏幕间导航,展示了在应用中切换屏幕的方法。

第二十章,实现数据列表,描述了如何在应用中实现数据列表。

第二十一章,地理位置和地图,解释了如何在应用中跟踪地理位置并添加地图。

第二十二章,收集用户输入,教你如何创建表单。

第二十三章,响应用户手势,提供了处理用户手势的示例。

第二十四章,显示进度,展示了如何处理进程指示器和进度条。

第二十五章,显示模态屏幕,教你如何创建对话框模态。

第二十六章,使用动画,描述了如何在应用中实现动画。

第二十七章,控制图像显示,概述了如何在 React Native 应用中渲染图像。

第二十八章,离线使用,展示了当手机没有互联网连接时如何处理应用。

为了充分利用这本书

本书假设你已对 JavaScript 编程语言有基本了解。它还假设你会跟随示例进行操作,这需要命令行终端、代码编辑器和网络浏览器。你将在第一章,为什么选择 React?中学习如何设置 React 项目。

学习 React Native 的要求与 React 开发相同,但要在真实设备上运行应用,你需要一部 Android 或 iOS 智能手机。为了在模拟器中运行 iOS 应用,你需要一台 Mac 电脑。要与 Android 模拟器一起工作,你可以使用任何类型的 PC。

下载示例代码文件

该书的代码包托管在 GitHub 上,网址为github.com/PacktPublishing/React-and-React-Native-5E。我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。去看看吧!

下载彩色图片

我们还提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/gbp/9781805127307

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“您实际声明的路由作为<Route>元素。”

代码块设置如下:

export default function First() {
  return <p>Feature 1, page 1</p>;
} 

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

export default function List({ data, fetchItems, refreshItems,
isRefreshing }) {
  return (
    <FlatList
data={data}
**renderItem****=****{({****item** **}) =>****<****Text****style****=****{styles.**
**item****}>****{item.value}****</****Text****>****}**
onEndReached={fetchItems} onRefresh={refreshItems} refreshing={isRefreshing}
/> );
} 

任何命令行输入或输出都按以下方式编写:

npm install @react-navigation/bottom-tabs @react-navigation/
drawer 

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“从管理面板中选择系统信息。”

警告或重要提示看起来是这样的。

小贴士和技巧看起来是这样的。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍的标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,点击提交勘误表,并填写表格。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了React 和 React Native,第五版,我们很乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都至关重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止于此,您将获得独家折扣、新闻通讯以及每天收件箱中的优质免费内容。

按照以下简单步骤获取福利:

  1. 扫描下面的二维码或访问以下链接:

packt.link/free-ebook/9781805127307

  1. 提交您的购买证明。

  2. 就这样!我们将直接将您的免费 PDF 和其他福利发送到您的电子邮件。

第一部分

React

在本部分,我们将介绍 React 工具和概念的基础,并将它们应用于构建高性能的 Web 应用。

在本部分,我们将涵盖以下章节:

  • 第一章为什么选择 React?

  • 第二章使用 JSX 进行渲染

  • 第三章理解 React 组件和 Hooks

  • 第四章以 React 的方式处理事件

  • 第五章构建可重用组件

  • 第六章使用 TypeScript 进行类型检查和验证

  • 第七章使用路由处理导航

  • 第八章使用懒加载组件和 Suspense 进行代码拆分

  • 第九章用户界面框架组件

  • 第十章高性能状态更新

  • 第十一章从服务器获取数据

  • 第十二章React 中的状态管理

  • 第十三章服务器端渲染

  • 第十四章React 中的单元测试

第一章:为什么选择 React?

如果你正在阅读这本书,你可能已经熟悉React了。但如果你不熟悉,不要担心。我会尽力将哲学定义保持到最小。然而,这是一本内容丰富的长书,所以我感觉设定基调是一个合适的第一步。我们的目标是学习 React 和React Native,但也是构建一个可扩展和适应性强的架构,可以处理我们今天和未来想要用 React 构建的一切。换句话说,我们想要围绕 React 建立一个基础,并配备一套额外的工具和方法,这些工具和方法能够经受时间的考验。本书将指导你使用诸如路由、TypeScript 类型、测试等工具的过程。

本章首先简要解释了 React 存在的理由。然后,我们将思考 React 的简洁性以及它如何能够处理许多网络开发者面临的典型性能问题。接下来,我们将探讨 React 的声明式哲学以及 React 程序员可以期望与之一起工作的抽象级别。然后,我们将简要介绍 React 的一些主要功能。最后,我们将探讨如何设置一个项目以开始使用 React。

一旦你对 React 及其如何解决 UI 开发问题有了概念性的理解,你将更有能力应对本书的其余部分。本章将涵盖以下主题:

  • 什么是 React?

  • React 的新功能有哪些?

  • 设置新的 React 项目

什么是 React?

我认为 React 在其主页上的一行描述(react.dev/)简洁且准确:

“一个用于构建用户界面的 JavaScript 库。”

这非常完美,因为事实证明,这是我们大多数时候想要的全部。我认为这个描述最好的部分就是它省略了所有内容。它不是一个大型框架。它不是一个全栈解决方案,将处理从数据库到通过WebSocket连接的实时更新的所有事情。我们可能实际上并不需要大多数这些预包装的解决方案。如果 React 不是一个框架,那么它究竟是什么呢?

React 只是视图层

通常认为 React 是应用程序中的视图层。应用程序通常分为不同的层,如视图层、逻辑层和数据层。在这个背景下,React 主要处理视图层,这涉及到根据数据和应用状态的变化来渲染和更新 UI。React 组件改变用户看到的内容。以下图表说明了 React 在我们前端代码中的位置:

图 1.1:React 应用程序的层级

这就是 React 的全部内容——核心概念。当然,随着我们阅读本书,这个主题可能会有细微的变化,但流程大致相同:

  1. 应用逻辑:从一些生成数据的应用逻辑开始

  2. 将数据渲染到 UI 上:下一步是将这些数据渲染到 UI 上。

  3. React 组件:为了实现这一点,您将数据传递给 React 组件。

  4. 组件的角色:React 组件承担将 HTML 放入页面的责任。

你可能会想这有什么大不了的;React 看起来只是另一种渲染技术。在章节的剩余部分,我们将讨论 React 在哪些关键领域可以简化应用程序开发。

简单是好的

React 没有太多需要学习和理解的部分。虽然 React 吹嘘有一个相对简单的 API,但重要的是要注意,在表面之下,React 以一定程度的复杂性运行。在这本书的整个过程中,我们将深入研究这些内部工作原理,探索 React 架构和机制的各个方面,以提供给您一个全面的理解。使用小型 API 的优势在于,您可以花更多的时间熟悉它,实验它,等等。相反,大型框架则不同,您所有的时间都用于弄清楚一切是如何工作的。以下图表为您提供了一个大致的概念,当使用 React 编程时,我们需要考虑哪些 API:

图 1.2:React API 的简单性

React 分为两个主要 API:

  • React 组件 API:这些是页面由React DOM渲染的部分。

  • React DOM:这是用于在网页上执行渲染的 API。

在 React 组件中,我们有以下区域需要考虑:

  • 数据:这是来自某处(组件并不关心它来自哪里)的数据,并由组件渲染。

  • 生命周期:例如,生命周期的一个阶段是组件即将被渲染的时候。在 React 组件中,方法或钩子会响应组件在 React 渲染过程中的进入和退出阶段,这些阶段随着时间的推移而发生。

  • 事件:这是我们编写的响应用户交互的代码。

  • JSX:这是在 React 组件中描述 UI 结构的常用语法。尽管 JSX 与 React 紧密相关,但它也可以与其他JavaScript框架和库一起使用。

不要一开始就专注于这些不同的 React API 区域代表什么。这里的要点是,React 本质上很简单。只需看看有多少东西需要弄清楚!这意味着我们不必花很多时间在这里详细研究 API。相反,一旦掌握了基础知识,我们就可以花更多的时间在符合声明式 UI 结构的细微 React 使用模式上。

声明式 UI 结构

React 新手往往难以理解组件如何将标记与 JavaScript 混合以声明 UI 结构。如果你看过 React 示例并产生了相同的负面反应,请不要担心。起初,我们可能会对这个方法持怀疑态度,我认为原因是我们几十年来一直被“关注点分离”原则所影响。这个原则指出,不同的关注点,如逻辑和展示,应该彼此分离。现在,每当我们看到事物结合在一起时,我们就会自动假设这是不好的,不应该发生。

React 组件使用的语法称为 JSX(即 JavaScript XML,也称为 JavaScript 语法扩展)。组件通过返回一些 JSX 来渲染内容。JSX 本身通常是 HTML 标记,混合了 React 组件的自定义标签。具体细节在此处并不重要;我们将在接下来的章节中详细介绍。

声明式 JSX 方法令人耳目一新的地方在于,我们不必手动执行复杂的操作来更改组件的内容。相反,我们描述 UI 在不同状态下的外观,React 高效地更新实际的 DOM 以匹配。因此,React UI 变得更容易和更高效地处理,从而带来更好的性能。

例如,想想使用 jQuery 来构建你的应用程序。你有一个页面,上面有一些内容,你想要在按钮点击时给一个段落添加一个类:

$(document).ready(function() {
  $('#my-button').click(function() {
    $('#my-paragraph').addClass('highlight');
  });
}); 

执行这些步骤足够简单。这被称为命令式编程,对于 UI 开发来说是有问题的。命令式编程在 UI 开发中的问题在于,它可能导致难以维护和修改的代码。这是因为命令式代码通常紧密耦合,意味着对代码某一部分的更改可能会在别处产生意外的后果。此外,命令式代码可能难以推理,因为它可能很难理解控制流的流动和应用程序在任何给定时间的状态。虽然这个更改元素类的例子很简单,但真实的应用程序往往需要超过三四个步骤才能实现某个功能。

React 组件不需要你以命令式的方式执行步骤。这就是 JSX 成为 React 组件核心的原因。XML 风格的语法使得描述 UI 应该是什么样子变得容易——也就是说,组件将要渲染哪些 HTML 元素?

export const App = () => {
  const [isHighlighted, setIsHighlighted] = useState(false);
  return (
    <div>
      <button onClick={() => setIsHighlighted(true)}>Add Class</button>
      <p className={isHighlighted && "highlight"}>This is paragraph</p>
    </div>
  );
}; 

在这个例子中,我们不仅仅是编写浏览器应该执行的命令式程序。这更像是一条指令,我们说明 UI 应该是什么样子,以及用户应该如何与之交互。这被称为声明式编程,非常适合 UI 开发。一旦你声明了你的 UI 结构,你需要指定它如何随时间变化。

数据会随时间变化

另一个对 React 新手来说难以掌握的领域是 JSX 就像一个静态字符串,代表了一块渲染输出的片段。这就是数据和时间的流逝发挥作用的地方。React 组件依赖于传入它们的数据。这些数据代表了 UI 的动态部分——例如,基于布尔值的渲染 UI 元素可能会在组件下一次渲染时发生变化。以下是一个说明这个概念的图示:

图 1.3:React 组件随时间变化

每次 React 组件渲染时,就像在那个确切时刻对 JSX 进行快照。随着你的应用程序随时间推移,你将有一个有序的渲染 UI 组件集合。除了声明性地描述 UI 应该是什么样子之外,重新渲染相同的 JSX 内容使开发者的工作变得更加容易。挑战在于确保 React 可以处理这种方法的性能需求。

性能很重要

使用 React 构建 UI 意味着我们可以用 JSX 声明 UI 的结构。这比逐个组装 UI 的命令式方法更不容易出错。然而,声明式方法在性能上确实提出了挑战。

例如,对于初始渲染来说,具有声明性 UI 结构是可行的,因为页面上还没有任何内容。所以 React 渲染器可以查看 JSX 中声明的结构,并在 DOM 浏览器中渲染它。

文档对象模型DOM)代表浏览器中渲染后的 HTML。DOM API 是 JavaScript 如何更改页面内容的方式。

以下图示说明了这个概念:

图 1.4:JSX 语法在浏览器 DOM 中如何转换为 HTML

在初始渲染时,React 组件及其 JSX 与其他模板库没有区别。例如,有一个名为Handlebars的模板库用于服务器端渲染,它将模板渲染为 HTML 标记字符串,然后将其插入到浏览器 DOM 中。React 与 Handlebars 等库的不同之处在于,React 可以在数据发生变化并且我们需要重新渲染组件时进行适应,而 Handlebars 将仅仅重新构建整个 HTML 字符串,就像它在初始渲染时做的那样。由于这对性能来说是个问题,我们通常最终会实现命令式的工作区,手动更新 DOM 的一小部分。我们最终会得到一个声明性模板和命令式代码的混乱组合,用于处理 UI 的动态方面。

我们在 React 中不这样做。这就是 React 与其他视图库不同的地方。组件在初始渲染时是声明性的,即使在它们重新渲染时也是如此。这就是 React 在底层如何使声明性 UI 结构的重新渲染成为可能。

然而,在 React 中,当我们创建一个组件时,我们清楚地描述了它应该看起来是什么样子。即使我们更新我们的组件,React 也会在幕后平滑地处理这些变化。换句话说,组件在初始渲染时是声明式的,即使在重新渲染时也是如此。这是可能的,因为 React 使用虚拟 DOM,它用于在内存中保持真实 DOM 元素的表示。它这样做是为了每次我们重新渲染一个组件时,它都可以将新内容与页面上已显示的内容进行比较。根据差异,虚拟 DOM 可以执行必要的命令步骤来做出更改。因此,当我们需要更新 UI 时,我们不仅保留了我们的声明式代码,React 还会确保它是高效完成的。以下是这个过程的样子:

图 1.5:React 将 JSX 语法转换为命令式 DOM API 调用

当您阅读有关 React 的内容时,您经常会看到诸如 diffingpatching 这样的词汇。Diffing 意味着比较 旧内容(UI 的先前状态)与 新内容(更新的状态)以识别差异,就像比较文档的两个版本以查看发生了什么变化一样。Patching 意味着执行必要的 DOM 操作以渲染新内容,确保只做出特定的更改,这对于性能至关重要。

与任何其他 JavaScript 库一样,React 受限于主线程的 运行至完成 特性。例如,如果 React 虚拟 DOM 逻辑正忙于比较内容并修补真实 DOM,浏览器就无法响应用户输入,如点击或交互。

在本章的下一节中,您将看到对 React 内部渲染算法进行了修改,以减轻这些性能陷阱。在解决了性能问题后,我们需要确保 React 足够灵活,能够适应我们未来可能希望部署应用程序的不同平台。

正确的抽象层次

在我们深入研究 React 代码之前,我想在较高层次上讨论另一个主题,那就是 抽象

在上一节中,您看到了 JSX 语法如何转换为低级操作来更新我们的 UI。React 将我们的声明式 UI 组件转换为更好的方式是,我们并不一定关心渲染目标是什么。在 React 中,渲染目标恰好是浏览器 DOM,但正如我们将要看到的,它并不局限于浏览器 DOM。

React 有潜力用于我们想要创建的任何 UI,在任何可想象设备上。我们只是刚开始看到 React Native,但可能性是无限的。我不会对React Toast(这根本不是一回事)突然变得相关而感到惊讶,其中 React 针对的是可以将 JSX 渲染输出烧焦到面包上的烤面包机。React 的抽象级别在多功能性和适应性之间取得了平衡,同时保持了实际和高效的用户界面开发方法。

以下图表展示了 React 如何针对不仅仅是浏览器:

图片

图 1.6:React 将目标渲染环境从我们实现的组件中抽象出来

从左到右,我们有React DOMReact NativeReact PDFReact Unity。所有这些 React 渲染器库都接受 React 组件并返回特定平台的输出。正如你所见,要针对新事物,相同的模式适用:

  • 实现特定于目标组件。

  • 实现一个 React 渲染器,使其能够在底层执行特定平台的操作。

这显然是对任何给定 React 环境中实际实现内容的过度简化。但对于我们来说,细节并不那么重要。重要的是,我们可以利用我们的 React 知识来专注于描述我们任何平台上的 UI 结构。

现在你已经了解了 React 中抽象的作用,让我们看看 React 的新特性。

React 的新特性是什么?

React 是一个在不断变化的 Web 开发领域中持续演进的库。当你开始学习并掌握 React 的旅程时,了解库的演变及其随时间的变化是很重要的。

React 的一个优点是,其核心 API 在近年来相对稳定。这提供了一种连续性,并允许开发者利用他们从先前版本中获得的知识。React 的概念基础保持完整,这意味着三五年前获得的技能今天仍然适用。让我们回顾一下,从 React 的早期版本到最近的版本,React 的历史。从React 0.xReact 18,已经进行了许多关键的变化和增强,如下所示:

  • React 0.14:在这个版本中,函数组件的引入允许开发者将函数用作组件,简化了基本 UI 元素的创建。当时,没有人知道现在我们只会编写函数组件,几乎完全放弃基于类的组件。

  • React 15:采用新的版本方案,React 15 的下一个更新对内部架构进行了全面的重构,从而提高了性能和稳定性。

  • React 16:然而,这个版本是 React 历史上最引人注目的发布之一。它引入了 hooks,这是一个革命性的概念,使开发者能够在不使用类组件的情况下使用状态和其他 React 功能。Hooks 使代码更简单、更易读,改变了开发者编写组件的方式。本书将探讨许多 hooks。此外,React 16 还引入了 Fiber,这是一种新的协调机制,显著提高了性能,尤其是在处理动画和复杂 UI 结构时。

  • React 17:这个版本专注于更新和维护与先前版本的兼容性。它引入了一个新的 JSX 转换系统。

  • React 18:这次发布继续了改进的轨迹,强调了性能提升和新增功能,例如渲染的自动批处理、状态转换、服务器组件和流式服务器端渲染。与性能相关的大部分重要更新将在第十二章高性能状态更新中进行探讨。关于服务器端渲染的更多细节将在第十四章使用 React 框架进行服务器端渲染和静态站点生成中介绍。

  • React 19:引入了几个主要功能和改进。React 编译器是一个新的编译器,它能够实现自动记忆化并优化重新渲染,消除了手动 useMemouseCallback 和记忆优化。增强的 Hooks,如用于数据获取的 use(promise)、用于表单处理的 useFormStatus()useFormState()、以及用于乐观 UI 的 useOptimistic() 简化了常见任务。React 19 还带来了简化的 API,例如 ref 成为常规属性、React.lazy 被取代,以及 Context.Provider 变为 Context。异步渲染允许在渲染过程中异步获取数据,而不会阻塞 UI,而错误处理改进提供了更好的机制来诊断和修复应用程序中的问题。

React 的稳定性和兼容性使其成为长期使用的可靠库,而持续的更新确保它始终处于网络和移动开发的前沿。在这本书中,所有示例都将使用最新的 React API,确保它们在未来版本中仍然功能性和相关性。

现在我们已经探讨了 React 的演变和更新,我们可以更深入地了解 React,并检查如何设置新的 React 项目。

设置新的 React 项目

在开始创建 React 项目时,有几种方法可以创建 React 项目。在本节中,我们将探讨三种常见的方法:

  • 使用网络打包器

  • 使用框架

  • 使用在线代码编辑器

    要开始开发和预览您的 React 应用程序,您首先需要在您的计算机上安装 Node.js。Node.js 是执行 JavaScript 代码的运行环境。

让我们接下来在以下子节中深入了解每种方法。

使用网络打包器

使用网络打包器是创建 React 项目的有效方法,尤其是如果你正在构建一个单页应用程序SPA)。在这本书的所有示例中,我们将使用Vite作为我们的网络打包器。Vite 以其卓越的速度和易于设置及使用而闻名。

要使用 Vite 设置你的项目,你需要采取以下步骤:

  1. 请确保你的计算机上已安装 Node.js,你可以通过访问官方 Node.js 网站 (nodejs.org/) 并下载适合你操作系统的相应版本来做到这一点。

  2. 打开你的终端或命令提示符,导航到你想要创建项目的目录:

    mkdir react-projects
    cd react-projects 
    
  3. 运行以下命令以使用 Vite 创建一个新的 React 项目:

    npm create vite@latest my-react-app -- --template react 
    

    此命令创建一个名为 my-react-app 的新目录,并使用 Vite 模板设置一个 React 项目。

  4. 一旦项目创建完成,你的终端应该如下所示:

    Scaffolding project in react-projects/my-react-app...
    Done. Now run:
      cd my-react-app
      npm install
         npm run dev 
    
  5. 导航到项目目录并安装依赖项。终端的结果应该如下所示:

    added 279 packages, and audited 280 packages in 21s
    103 packages are looking for funding
      run 'npm fund' for details
    found 0 vulnerabilities 
    

最后,通过运行以下命令启动开发服务器:npm run dev

此命令启动开发服务器,你可以通过打开浏览器并访问 http://localhost:3000 来查看你的 React 应用程序。

到目前为止,你已经成功使用 Vite 作为网络打包器设置了你的 React 项目。有关 Vite 及其可能的配置的更多信息,请访问官方网站 vitejs.dev/

使用框架

对于现实世界和商业项目,建议使用构建在 React 之上的框架。这些框架提供了额外的功能,例如路由和资产管理(图像、SVG 文件、字体等)。它们还指导你有效地组织项目结构,因为框架通常强制执行特定的文件组织规则。一些流行的 React 框架包括Next.jsGatsbyRemix

第十三章服务器端渲染中,我们将探讨设置 Next.js 以及它与使用普通网络打包器之间的差异。

在线代码编辑器

在线代码编辑器结合了网络打包器和框架的优点,但允许你在云端或直接在浏览器中设置你的 React 开发环境。这消除了在你的机器上安装任何东西的需要,并让你可以直接在浏览器中编写和探索 React 代码。

虽然有各种在线代码编辑器可供选择,其中一些最受欢迎的选项包括CodeSandboxStackBlitzReplit。这些平台提供了一个用户友好的界面,并允许你创建、分享和协作 React 项目,而无需任何本地设置。

要开始使用在线代码编辑器,您甚至不需要账户。只需在浏览器上遵循此链接:react.new。几秒钟后,您将看到 CodeSandbox 已经准备好使用模板项目进行工作,并且编辑器的实时预览可以直接在浏览器标签中查看。如果您想保存更改,则需要创建账户。

使用在线代码编辑器是学习和实验 React 的便捷方式,尤其是如果您更喜欢基于浏览器的开发环境。

在本节中,我们探讨了不同的方法来设置您的 React 项目。无论您选择 Web 打包器、框架还是在线代码编辑器,每种方法都提供了其独特的优势。选择您喜欢且适合您项目需求的方法。现在,我们准备好进入 React 开发的领域了!

摘要

在本章中,我们全面介绍了 React,以便您对其有一个大致的了解以及其必要的方面,为本书的其余部分定下基调。React 是一个具有小型 API 的库,用于构建 UI。然后,我们向您介绍了 React 的一些关键概念。我们讨论了 React 之所以简单,是因为它没有很多可移动部件。

之后,我们探讨了 React 组件和 JSX 的声明性本质。在此之后,您了解到 React 通过编写可重复渲染的声明性代码来实现有效的性能。

您还深入了解了渲染目标的概念以及 React 如何轻松成为各种平台的首选 UI 工具。然后,我们为您提供了 React 历史的简要概述并介绍了最新进展。最后,我们深入探讨了如何设置新的 React 项目并启动学习过程。

目前,这些就足够作为入门和概念性内容了。随着我们继续阅读本书,我们将重新审视这些概念。接下来,让我们退后一步,牢固掌握基础知识,从下一章使用 JSX 进行渲染开始。

加入我们的 Discord 吧!

与其他用户和作者一起阅读本书。提出问题,为其他读者提供解决方案,与作者聊天等等。扫描二维码或访问链接加入社区。

packt.link/ReactAndReactNative5e

第二章:使用 JSX 渲染

本章将向您介绍 JSX,这是一种嵌入在您的 JavaScript 代码中的 XML/HTML 标记语法,用于声明您的 React 组件。在最基本的层面上,您将使用 HTML 标记来描述您的 UI 的各个部分。构建 React 应用程序涉及将这些 HTML 标记片段组织成组件。在 React 中,创建一个组件允许您定义超出基本 HTML 标记的自定义元素。这些自定义元素或组件使用 JSX 定义,然后将其转换为浏览器可以识别的标准 HTML 元素。能够创建和重用自定义组件是 React 的核心特性,它使得更动态和复杂的 UI 成为可能。这正是 React 引人入胜的地方——拥有自己的 JSX 标签,可以使用 JavaScript 表达式使您的组件生动起来。JSX 是用于描述使用 React 构建的 UI 的语言。

在本章中,我们将涵盖以下内容:

  • 您的第一个 JSX 内容

  • 渲染 HTML

  • 创建您自己的 JSX 元素

  • 使用 JavaScript 表达式

  • 构建 JSX 片段

技术要求

本章的代码可以在配套 GitHub 仓库的以下目录中找到:github.com/PacktPublishing/React-and-React-Native-5E/tree/main/Chapter02

您的第一个 JSX 内容

在本节中,我们将实现必做的 Hello World JSX 应用程序。这个初步的探索只是开始——这是一个简单而有效的方法来熟悉语法及其功能。随着我们的进展,我们将深入研究更复杂和细微的示例,展示 JSX 在构建 React 应用程序中的强大和灵活性。我们还将讨论是什么使得这种语法适用于声明式 UI 结构。

Hello JSX

不再拖延,这是您的第一个 JSX 应用程序:

import * as ReactDOM from "react-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <p>
    Hello, <strong>JSX</strong>
  </p>
); 

让我们来看看这里发生了什么。

render() 函数接受 JSX 作为参数,并将其渲染到传递给 ReactDOM.createRoot() 的 DOM 节点。

在此示例中,实际的 JSX 内容渲染了一个包含一些粗体文本的段落。这里没有发生什么特别的事情,所以我们完全可以直接将这个标记作为纯字符串插入到 DOM 中。然而,此示例的目的是展示将 JSX 渲染到页面上的基本步骤。

在底层,JSX 并不是由网络浏览器直接理解的,需要转换成浏览器可以执行的标准的 JavaScript 代码。这种转换通常使用像 ViteBabel 这样的工具来完成。当 Vite 处理 JSX 代码时,它会将 JSX 编译成 React.createElement() 调用。这些调用创建代表虚拟 DOM 元素的 JavaScript 对象。例如,上面示例中的 JSX 表达式被编译成这样:

import * as ReactDOM from "react-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  React.createElement(
    "p",
    null,
    "Hello, ",
    React.createElement("strong", null, "JSX")
  )
); 

React.createElement 的第一个参数是元素的类型(例如,用于 DOM 元素的字符串 divp,或用于复合组件的 React 组件)。第二个参数是一个包含此元素属性的对象,任何后续参数都是此元素的子元素。这种转换由 Vite 在幕后完成,你永远不会编写这样的代码。

React.createElement() 创建的这些对象,称为 React 元素,以对象格式描述了 UI 组件的结构和属性,React 可以处理这些对象。然后,React 使用这些对象构建实际的 DOM 并保持其更新。这个过程涉及一个协调算法,它以高效的方式更新 DOM 以匹配 React 元素。当组件的状态发生变化时,React 计算出更新 DOM 所需的最小更改集,而不是重新渲染整个组件。这使得更新更加高效,也是使用 React 的关键优势之一。

在我们继续更深入的代码示例之前,让我们花一点时间回顾一下我们的 Hello World 示例。JSX 内容简短且简单。它也是声明性的,因为它描述了要渲染的内容,而不是如何渲染。具体来说,通过查看 JSX,你可以看到这个组件将渲染一个段落和一些粗体文本。如果以命令式方式完成,可能需要更多步骤,并且它们可能需要按特定顺序执行。

我们刚刚实现的示例应该让你对声明性 React 有一定的感觉。随着我们在本章以及整本书的进展,JSX 标记将变得更加复杂。然而,它始终会描述 UI 中的内容。

render() 函数告诉 React 以最有效的方式将你的 JSX 标记更新到 UI 上。这就是 React 如何让你能够声明 UI 的结构,而无需考虑执行更新屏幕上元素的有序步骤,这种方法往往会导致错误。React 默认支持任何 HTML 页面上都能找到的标准 HTML 标签,例如 divph1ulli 等。

现在我们已经发现了 JSX 是什么,它是如何工作的,以及它遵循的声明性理念,让我们探索如何渲染纯 HTML 标记,以及我们应该遵循哪些约定。

渲染 HTML

最终,React 组件的职责是在 DOM 浏览器中渲染 HTML。这就是为什么 JSX 默认支持 HTML 标签。在本节中,我们将查看一些渲染可用 HTML 标签的代码。然后,我们将介绍在 React 项目中使用 HTML 标签时通常遵循的一些约定。

内置 HTML 标签

当我们渲染 JSX 时,元素标签引用 React 组件。由于为 HTML 元素创建组件会很麻烦,React 提供了 HTML 组件。我们可以在 JSX 中渲染任何 HTML 标签,输出将正如我们所期望的那样。

现在,让我们尝试渲染一些这些标签:

import * as ReactDOM from "react-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <div>
    <button />
    <code />
    <input />
    <label />
    <p />
    <pre />
    <select />
    <table />
    <ul />	
  </div>
); 

对于这个示例,不必担心渲染输出的格式。我们确保可以渲染任意 HTML 标签,并且它们会按照预期渲染,无需任何特殊的定义和导入。

你可能已经注意到了周围的 <div> 标签,它将所有其他标签作为其子元素分组。这是因为 React 需要一个根元素来渲染。在章节的后面,你将学习如何在不将相邻元素包裹在父元素中的情况下渲染它们。

使用 JSX 渲染的 HTML 元素紧密遵循常规 HTML 元素语法,但在大小写敏感性和属性方面有一些细微的差异。

HTML 标签约定

当你在 JSX 标记中渲染 HTML 标签时,预期你会使用小写字母来表示标签名。实际上,将 HTML 标签名称大写将会失败。标签名是区分大小写的,并且非 HTML 元素名称是大写的。这样,就可以轻松地扫描标记并区分内置的 HTML 元素和其他所有元素。

你还可以传递 HTML 元素任何它们的标准属性。当你传递它们意料之外的东西时,会记录一条关于未知属性的警告。以下是一个说明这些概念的示例:

import * as ReactDOM from "react-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <button title="My Button" foo="bar">
    My Button
  </button>
);
root.render(<Button />); 

当你运行这个示例时,它将无法编译,因为 React 不了解 <Button> 元素;它只知道 <button>

你可以使用任何有效的 HTML 标签作为 JSX 标签,只要你记住它们是区分大小写的,并且需要传递正确的属性名称。除了只有属性值的简单 HTML 标签外,你还可以使用更语义化的 HTML 标签来描述页面内容的结构。

描述 UI 结构

JSX 能够以将它们组合在一起形成完整 UI 结构的方式描述屏幕元素。让我们看看一些声明比单个段落更复杂结构的 JSX 标记:

import * as ReactDOM from "react-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <section>
    <header>
      <h1>A Header</h1>
    </header>
    <nav>
      <a href="item">Nav Item</a>
    </nav>
    <main>
      <p>The main content...</p>
    </main>
    <footer>
      <small>&copy; 2024</small>
    </footer>
  </section>
); 

这个 JSX 标记描述了一个相当复杂的 UI 结构。然而,由于它是 HTML,HTML 适合简洁地表达层次结构,因此它比命令式代码更容易阅读。这就是我们希望在 UI 需要改变时如何思考——不是作为一个单独的元素或属性,而是作为一个整体 UI。

下面是渲染后的内容看起来像什么:

图片

图 2.1:使用 JSX 语法描述 HTML 标签结构

在这个标记中有许多描述 UI 结构的语义元素。例如,<header> 元素描述了标题所在的页面顶部部分,而 <main> 元素描述了主要页面内容所在的位置。这种复杂结构使开发者更容易推理。但在我们开始实现动态 JSX 标记之前,让我们创建一些自己的 JSX 组件。

创建自己的 JSX 元素

组件是 React 的基本构建块。实际上,它们可以被看作是 JSX 标记的词汇表,允许你通过可重用、封装的元素创建复杂界面。在本节中,我们将深入了解如何创建自己的组件并在其中封装 HTML 标记。

封装 HTML

我们创建新的 JSX 元素,以便我们可以封装更大的结构。这意味着我们不需要输入复杂的标记,可以使用自定义标签。React 组件返回用于标签位置的 JSX。让我们看看以下示例:

import * as ReactDOM from "react-dom";
function MyComponent() {
  return (
    <section>
      <h1>My Component</h1>
      <p>Content in my component...</p>
    </section>
  );
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<MyComponent />); 

这就是渲染后的输出效果:

图片

图 2.2:一个渲染封装 HTML 标记的组件

这是我们在 React 中实现的第一个组件,所以让我们花点时间分析一下这里发生了什么。我们创建了一个名为 MyComponent 的函数,在它的返回语句中我们放置了我们的 HTML 标签。这就是我们创建一个用作新 JSX 元素的 React 组件的方法。正如你在对 render() 的调用中看到的那样,你正在渲染一个 <MyComponent> 元素。

这个组件封装的 HTML 是从我们创建的函数中返回的。在这种情况下,当 JSX 由 react-dom 渲染时,它被一个 <section> 元素和它内部的所有内容所替换。

当 React 渲染 JSX 时,你使用的任何自定义元素都必须在相同的作用域内有相应的 React 组件。在前面的示例中,MyComponent 函数是在与 render() 调用相同的作用域中声明的,所以一切按预期工作。通常,你会导入组件,将它们添加到适当的作用域中。随着你通过本书的进展,你会看到更多关于这一点的内容。

HTML 元素如 <div> 常常包含嵌套子元素。让我们看看我们是否可以用我们创建的 JSX 元素做到同样的事情,这些元素是通过实现组件来创建的。

嵌套元素

使用 JSX 标记描述具有父子关系的 UI 结构是有用的。子元素是通过在另一个组件(父组件)内部嵌套它们来创建的。

例如,一个 <li> 标签仅当它是 <ul> 标签或 <ol> 标签的子元素时才是有效的 – 你可能也会用你自己的 React 组件创建类似的嵌套结构。为此,你需要使用 children 属性。让我们看看这是如何工作的。以下是 JSX 标记:

import * as ReactDOM from "react-dom";
import MySection from "./MySection";
import MyButton from "./MyButton";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <MySection>
    <MyButton>My Button Text</MyButton>
  </MySection>
); 

你正在导入你自己的两个 React 组件:MySectionMyButton

现在,如果你查看 JSX 标记,你会注意到<MyButton><MySection>的子元素。你也会注意到MyButton组件接受文本作为其子元素,而不是更多的 JSX 元素。

让我们看看这些组件是如何工作的,从MySection开始:

export default function MySection(props) {
  return (
    <section>
      <h2>My Section</h2>
      {props.children}
    </section>
  );
} 

此组件渲染一个标准的<section>HTML 元素,一个标题,然后是{props.children}。正是这个最后的部分允许组件访问嵌套元素或文本并将它们渲染出来。

在前面的例子中使用的两个大括号用于 JavaScript 表达式。我将在下一节中详细介绍 JSX 标记中找到的 JavaScript 表达式语法的更多细节。

现在,让我们看看MyButton组件:

export default function MyButton(props) {
  return <button>{props.children}</button>;
} 

此组件使用与MySection完全相同的模式;它获取{props.children}的值并将其包裹在标记中。React 会为你处理细节。在这个例子中,按钮文本是MyButton的子元素,而MyButton又是MySection的子元素。然而,按钮文本通过MySection透明地传递。换句话说,我们不需要在MySection中编写任何代码来确保MyButton获取其文本。*真是太酷了,对吧?*下面是渲染输出的样子:

图 2.3:使用子 JSX 值渲染的按钮元素

现在,你已经知道了如何构建自己的 React 组件,在标记中引入新的 JSX 标签。我们在本章中查看的组件到目前为止都是静态的。也就是说,一旦我们渲染了它们,它们就再也没有更新过。JavaScript 表达式是 JSX 的动态部分,根据条件给出不同的输出。

使用 JavaScript 表达式

正如你在前面的章节中看到的,JSX 有一个特殊的语法,允许你嵌入 JavaScript 表达式。每当 React 渲染 JSX 内容时,标记中的表达式都会被评估。这个特性是 JSX 动态性的核心;它使得组件的内容和属性能够根据不同的数据或状态条件进行变化。每次 React 渲染或重新渲染 JSX 内容时,这些嵌入的表达式都会被评估,使得显示的 UI 能够反映当前的数据和状态。你还将学习如何将数据集合映射到 JSX 元素。

动态属性值和文本

一些 HTML 属性或文本值是静态的,这意味着当 JSX 标记重新渲染时,它们不会改变。其他值,即属性或文本的值,基于在应用程序其他地方找到的数据。记住,React 只是视图层。让我们看一个例子,这样你就可以感受到 JSX 标记中 JavaScript 表达式语法的样子:

import * as ReactDOM from "react-dom";
const enabled = false;
const text = "A Button";
const placeholder = "input value...";
const size = 50;
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <section>
    <button disabled={!enabled}>{text}</button>
    <input placeholder={placeholder} size={size} />
  </section>
); 

任何有效的 JavaScript 表达式,包括嵌套的 JSX,都可以放在大括号{}之间。对于属性和文本,这通常是变量名或对象属性。注意,在这个例子中,!enabled表达式计算出一个布尔值。下面是渲染输出的样子:

图 2.4:动态更改按钮的属性值

如果你正在跟随可下载的配套代码,我强烈推荐这样做,请尝试调整这些值,看看渲染的 HTML 如何变化:github.com/PacktPublishing/React-and-React-Native-5E/tree/main/Chapter02

原始的 JavaScript 值在 JSX 语法中使用起来非常直接。显然,我们可以在 JSX 中使用更复杂的数据类型,例如对象和数组,以及函数来处理事件。让我们来探讨一下。

事件处理

在 React 中,你可以轻松地将函数传递给组件的属性,以处理用户交互,如按钮点击、表单提交和鼠标移动。这允许你创建交互式和响应式的用户界面。React 提供了一种方便的方法,可以直接使用类似 addEventListenerremoveEventListener 方法在传统 JavaScript 中使用的语法将事件处理器附加到组件上。

为了说明这一点,让我们考虑一个例子,其中我们想在 React 组件中处理一个按钮点击事件:

import * as ReactDOM from "react-dom";
const handleClick = () => {
  console.log("Button clicked!");
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <section>
    <button onClick={handleClick}>Click me</button>
  </section>
); 

在这个例子中,我们定义了一个名为 handleClick 的函数,当按钮被点击时会调用该函数。然后我们将这个函数作为事件处理器附加到 <button> 组件的 onClick 属性上。每当按钮被点击时,React 将调用 handleClick 函数。

与在传统 JavaScript 中使用 addEventListenerremoveEventListener 相比,React 抽象掉了一些复杂性。使用 React 的事件处理,你不必担心手动将事件监听器附加到或从 DOM 元素中移除。React 管理着事件委托,并为组件内部的事件处理提供了一种更声明式的方法。

React 默认实现事件委托以优化性能。它不是将事件处理器附加到每个单独的元素上,而是将单个事件处理器附加到应用程序的根(或父组件)上。当一个事件在子元素上触发时,它会在组件树中向上冒泡,直到达到具有事件处理器的父组件。然后 React 的合成事件系统根据事件对象的 target 属性确定哪个组件应该处理该事件。这允许 React 高效地管理事件,而无需将处理器附加到每个元素上。

通过使用这种方法,你可以轻松地将事件传递给子组件,在父组件中处理它们,甚至通过多层嵌套组件传播事件。这有助于构建模块化和可重用的组件架构。我们将在下一章中看到这一点。

除了onClick事件外,React 还支持许多其他事件,如onChangeonSubmitonMouseOver以及所有标准事件。你可以将事件处理器附加到各种元素上,如按钮、输入字段、复选框等。

注意,React 提倡单向数据流,这意味着数据从父组件流向子组件。要从子组件向父组件传递数据或信息,你可以定义回调作为 props,并用必要的数据调用它们。在本书的后续章节中,我们将更深入地探讨 React 中的事件处理以及如何创建自定义回调。

将集合映射到元素

有时候,你需要编写 JavaScript 表达式来改变你的标记结构。在前面的章节中,你学习了如何使用 JavaScript 表达式语法动态更改 JSX 元素的属性值。那么,当你需要根据 JavaScript 集合添加或删除元素时怎么办呢?

在整本书中,当我提到 JavaScript 集合时,我指的是普通对象和数组,或者更普遍地说,任何可迭代的东西。

动态控制 JSX 元素的最佳方式是将它们从集合中映射出来。让我们看看如何做到这一点的一个例子:

import * as ReactDOM from "react-dom";
const array = ["First", "Second", "Third"];
const object = {
  first: 1,
  second: 2,
  third: 3,
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <section>
    <h1>Array</h1>
    <ul>
      {array.map((i) => (
        <li key={i}>{i}</li>
      ))}
    </ul>
    <h1>Object</h1>
    <ul>
      {Object.keys(object).map((i) => (
        <li key={i}>
          <strong>{i}: </strong>
          {object[i]}
        </li>
      ))}
    </ul>
  </section>
); 

第一个集合是一个名为array的数组,其中包含字符串值。向下到 JSX 标记,你可以看到对array.map()的调用,它返回一个新数组。映射函数实际上返回一个 JSX 元素(<li>),这意味着数组中的每个项目现在都在标记中表示。

评估这个表达式的结果是数组。不用担心——JSX 知道如何渲染元素数组。为了提高性能,给数组中的每个组件分配一个唯一的key prop 至关重要,这样 React 就可以在后续的重新渲染中高效地管理更新。

对象集合使用相同的技巧,但你必须调用Object.keys()然后映射这个数组。将集合映射到页面上的 JSX 元素的好处是,你可以根据收集到的数据来控制 React 组件的结构。

这意味着你不必依赖于命令式逻辑来控制 UI。

下面是渲染输出的样子:

图片

图 2.5:将 JavaScript 集合映射到 HTML 元素的结果

JavaScript 表达式让 JSX 内容生动起来。React 评估表达式,并根据已经渲染的内容和变化更新 HTML 内容。理解如何利用这些表达式非常重要,因为这是任何 React 开发者日常活动中最常见的事情之一。

现在,是时候学习如何在不依赖 HTML 标签的情况下将 JSX 标记分组在一起了。

构建 JSX 片段

片段是一种将标记块组合在一起的方法,而无需向你的页面添加不必要的结构。例如,一个常见的做法是让 React 组件返回被 <div> 元素包裹的内容。这个元素没有任何实际用途,只会给 DOM 增加杂乱。

让我们看看一个例子。这里有组件的两个版本。一个使用包装元素,另一个使用新的片段功能:

import * as ReactDOM from "react-dom";
import WithoutFragments from "./WithoutFragments";
import WithFragments from "./WithFragments";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <div>
    <WithoutFragments />
    <WithFragments />
  </div>
); 

渲染的两个元素是 <WithoutFragments><WithFragments>。以下是它们渲染后的样子:

图 2.6:片段有助于渲染更少的 HTML 标签,而没有任何视觉差异

现在让我们比较这两种方法。

使用包装元素

第一种方法是将兄弟元素包裹在 <div> 中。以下是源代码的样子:

export default function WithoutFragments() {
  return (
    <div>
      <h1>Without Fragments</h1>
      <p>
        Adds an extra <code>div</code> element.
      </p>
    </div>
  );
} 

这个组件的本质是 <h1><p> 标签。然而,为了从 render() 方法中返回它们,你必须用 <div> 标签将它们包裹起来。实际上,使用浏览器开发者工具检查 DOM 会发现 <div> 标签除了添加另一个结构层级外,并没有做任何事情:

图 2.7:DOM 中的另一个结构层级

现在,想象一个包含大量这些组件的应用——那将有很多无用的元素!让我们看看如何使用片段来避免不必要的标签。

使用片段

让我们看看 WithFragments 组件,其中我们避免了使用不必要的标签:

export default function WithFragments() {
  return (
    <>
      <h1>With Fragments</h1>
      <p>Doesn't have any unused DOM elements.</p>
    </>
  );
} 

与将组件内容包裹在 <div> 中的做法不同,这里使用的是 <> 元素。这是一个特殊的元素类型,表示只需要渲染其子元素。<>React.Fragment 组件的简写。如果你需要向片段传递一个键属性,就不能使用 <> 语法。

如果你检查 DOM,可以看到与 WithoutFragments 组件相比的差异。

图 2.8:片段中的更少 HTML

随着在 JSX 标记中使用片段的出现,页面上渲染的 HTML 更少了,因为我们不需要使用 <div> 等标签来仅仅是为了将元素组合在一起。相反,当组件渲染片段时,React 会知道在组件被使用的地方渲染片段的子元素。

因此,片段使 React 组件能够仅渲染必要的元素;不再会有无用的元素出现在渲染的页面上。

概述

在本章中,你学习了 JSX 的基础知识,包括其声明性结构,这有助于编写更易于维护的代码。然后,你编写了一些代码来渲染基本的 HTML,并学习了如何使用 JSX 描述复杂结构;每个 React 应用至少包含一些结构。

然后,你花了一些时间学习如何通过实现自己的 React 组件来扩展 JSX 标记的词汇量,这就是你如何将 UI 设计为一系列较小的部分并将它们粘合在一起形成一个整体的方式。接着,你学习了如何将动态内容引入 JSX 元素属性,以及如何将 JavaScript 集合映射到 JSX 元素,从而消除了控制 UI 显示的命令式逻辑的需求。最后,你学习了如何渲染 JSX 内容的片段,这可以防止使用不必要的 HTML 元素。

现在你已经对通过在 JavaScript 模块中嵌入声明性 XML 来渲染 UI 的感觉有了了解,是时候进入下一章了,我们将更深入地探讨组件、属性和状态。

第三章:理解 React 组件和 Hooks

在本章中,我们将深入探讨 React 组件及其基本方面,并介绍Hooks的强大功能。

我们将探讨组件数据的基本概念以及它是如何塑造你的 React 应用结构的。我们将讨论两种主要的组件数据类型:属性状态。属性允许我们向组件传递数据,而状态使组件能够动态地管理和更新其内部数据。我们将探讨这些概念如何应用于函数组件,并说明设置组件状态和传递属性的工作原理。

在本章中,我们将涵盖以下主题:

  • React 组件简介

  • 组件属性是什么?

  • 组件状态是什么?

  • React Hooks

  • 使用 Hooks 维护状态

  • 执行初始化和清理操作

  • 使用 context Hooks 共享数据

  • 使用 Hooks 进行记忆化

技术要求

本章的代码可以在以下链接找到:github.com/PacktPublishing/React-and-React-Native-5E/tree/main/Chapter03

React 组件简介

React 组件是现代 Web 和移动应用的基础构建块。它们封装了可重用的代码部分,用于定义用户界面不同部分的架构、行为和外观。通过将 UI 分解成更小、更独立的组件,React 使开发者能够创建可扩展、可维护和交互式的应用。

在本质上,React 组件是一个返回类似 HTML 标记的 JSX 语法的 JavaScript 函数或类。在本书中,我们将主要关注函数组件,因为它们在近年来已成为构建组件的首选方法。与类组件相比,函数组件更简单、更简洁、更容易理解。它们利用 JavaScript 函数的力量,并利用 React Hooks 来管理状态和执行副作用。

在 React 中使用组件的主要优势之一是它们的可重用性。组件可以在应用的多个部分中重用,减少代码重复并提高开发效率。此外,组件促进了模块化开发方法,使开发者能够将复杂的 UI 分解成更小、更易于管理的部分。

组件属性是什么?

在 React 中,组件属性,通常称为props,允许我们将数据从父组件传递到其子组件。Props 提供了一种定制和配置组件的方式,使它们灵活且可重用。Props 是只读的,这意味着子组件不应直接修改它们。相反,父组件可以更新 props 值并触发子组件的重新渲染,以使用更新的数据。

当定义一个函数组件时,你可以将其作为参数访问传递给它的 props:

const MyComponent = (props) => {
  return (
    <div>
      <h1>{props.title}</h1>
      <p>{props.description}</p>
    </div>
  );
}; 

在上面的例子中,MyComponent函数组件将 props 对象作为参数接收。我们可以通过点符号访问单个属性,例如props.titleprops.description,以在组件的 JSX 标记中渲染数据。也可以通过解构来访问属性:

const MyComponent = ({ title, description }) => {
  return (
    <div>
      <h1>{title}</h1>
      <p>{description}</p>
    </div>
  );
}; 

如你所见,这种方法甚至更简洁,还允许我们使用另一个解构特性,默认值,我们将在本章中讨论。

传递属性值

React 组件属性是通过在渲染组件时传递 JSX 属性来设置的。在第七章Type Checking and Validation with TypeScript中,我将更详细地介绍如何验证传递给组件的属性值。现在,让我们创建一些额外的组件,除了MyComponent之外,它们期望不同类型的属性值:

const MyButton = ({ disabled, text }) => {
  return <button disabled={disabled}>{text}</button>;
}; 

这个简单的按钮组件期望一个布尔值禁用属性和一个字符串文本属性。当我们创建组件来展示如何传递以下属性时,你会注意到我们已经在按钮 HTML 元素中传递了这些属性:

  • 禁用属性:我们将其放入按钮属性中,属性名为disabled

  • 文本属性:我们将其作为子属性传递给按钮

还重要的是要知道,你想要传递给组件的任何 JavaScript 表达式都应该用大括号括起来。

让我们再创建一个期望数组属性值的组件:

const MyList = ({ items }) => (
  <ul>
    {items.map((i) => (
      <li key={i}>{i}</li>
    ))}
  </ul>
); 

你可以通过 JSX 传递几乎任何你想要的属性值,只要它是一个有效的 JavaScript 表达式。MyList组件接受一个 items 属性,一个映射到<li>元素的数组。

现在,让我们编写一些代码来设置这些属性值:

import * as ReactDOM from "react-dom";
import MyButton from "./MyButton";
import MyList from "./MyList";
import MyComponent from "./MyComponent";
const root = ReactDOM.createRoot(document.getElementById("root"));
const appState = {
  text: "My Button",
  disabled: true,
  items: ["First", "Second", "Third"],
};
function render(props) {
  root.render(
    <main>
      <MyComponent
        title="Welcome to My App"
        description="This is a sample component."
      />
      <MyButton text={props.text} disabled={props.disabled} />
      <MyButton text="Another Button" disabled />
      <MyList items={props.items} />
    </main>
  );
}
render(appState);
setTimeout(() => {
  appState.disabled = false;
  appState.items.push("Fourth");
  render(appState);
}, 1000); 

render函数看起来每次被调用时都会创建新的 React 组件实例。React 足够智能,能够判断这些组件已经存在,并且它只需要确定新属性值将导致输出差异。在这个例子中,setTimeout的调用导致 1 秒的延迟。然后,appState.disabled的值被更改为 false,appState.items数组末尾添加了一个新值。对render的调用将使用新的属性值重新渲染组件。

从这个例子中,我们还可以得到一个启示,即你有一个appState对象,它保留着应用程序的状态。当组件渲染时,这个状态的一部分会被作为属性传递给组件。状态必须存在于某个地方,在这个例子中,它位于组件之外。我们将在第十二章React 中的状态管理中深入探讨这种方法,以及为什么它很重要。

希望你已经注意到我们渲染了另一个按钮,我们以不同的方式传递了 props:

<MyButton text="Another Button" disabled /> 

这是一个有效的 JSX 表达式,如果我们想向组件传递常量值,我们可以传递不带花括号的字符串,并传递布尔值 true,只需在组件中留下属性名即可。

默认属性值

除了传递数据之外,我们还可以使用 defaultProps 属性指定属性的默认值。当属性未提供时,这很有用,确保组件仍然表现正确:

const MyButton = ({ disabled, text }) => (
  <button disabled={disabled}>{text}</button>
);
MyButton.defaultProps = {
  disabled: false,
  text: "My Button",
}; 

在此情况下,如果父组件没有提供 textdisabled 属性,组件将回退到在 defaultProps 中指定的默认值。

如我之前所述,使用解构,我们有更方便的方式来设置默认属性。

让我们看看 MyButton 组件的更新示例:

const MyButton = ({ disabled = false, text = "My Button" }) => (
  <button disabled={disabled}>{text}</button>
); 

使用解构,我们可以在函数内部定义属性并设置默认值。当组件有很多属性时,这更简洁且易于查看。

在接下来的章节中,我们将进一步探讨使用 Hooks 和其他关键概念的组件状态。

什么是组件状态?

在 React 中,组件状态指的是组件持有的内部数据。它代表可以在组件内部使用并可随时间更新的可变值。状态允许组件跟踪可能发生变化的信息,例如用户输入、API 响应或任何需要动态和响应式的其他数据。

状态是 React 提供的一个特性,它使组件能够管理和更新它们自己的数据。它允许组件在状态变化时重新渲染,确保用户界面反映了最新的数据。

要在 React 组件中定义状态,你应该在组件内部使用 useState hook。然后你可以在组件的方法或 JSX 代码中访问和修改状态。当状态更新时,React 会自动重新渲染组件及其子组件以反映这些更改。

在跳转到组件中使用状态的示例之前,让我们简要地探索一下 React hook 是什么。

React Hooks

React Hooks 是在 React 16.8 中引入的一个特性,它允许你在函数组件中使用状态和其他 React 特性。在 Hooks 之前,状态管理和生命周期方法主要在类组件中使用。Hooks 提供了一种在函数组件中实现类似功能的方法,使它们更强大、更容易编写和理解。

Hooks 是一些函数,它们使你能够“挂钩”到 React 的内部特性,例如状态管理、上下文、效果等。它们以 use 关键字为前缀(例如 useStateuseEffectuseContext 等)。React 提供了几个内置的 Hooks,你也可以创建自定义 Hooks 来封装可重用的状态逻辑。

最常用的内置 Hooks 包括:

  • useState:这个钩子允许你在函数式组件中添加状态。它返回一个包含两个元素的数组:当前状态值和一个用于更新状态的函数。

  • useEffect:这个钩子允许你在组件中执行副作用,例如获取数据、订阅事件或手动操作 DOM。它默认在每次渲染后运行,可以用来处理组件的生命周期事件,比如组件挂载、更新或卸载。

  • useContext:这个钩子允许你从 React 上下文中消费值。它提供了一种方法来访问上下文值,而无需嵌套多个组件。

  • useCallbackuseMemo:这些钩子用于性能优化。useCallback 缓存了一个函数,防止它在每次渲染时被重新创建,而 useMemo 缓存了一个值,只有当它的依赖项改变时才会重新计算。

我们将在本章中检查所有这些钩子,并在整本书中使用它们。让我们继续讨论状态,并探索如何使用 useState 钩子来管理它。

使用 Hooks 维护状态

我们将要查看的第一个 React 钩子 API 被称为 useState,它使你的函数式 React 组件能够拥有状态。在本节中,你将学习如何使用 Hooks 初始化状态值并改变组件的状态。

初始状态值

当我们的组件首次渲染时,它们可能期望设置一些状态值。这被称为组件的初始状态,我们可以使用 useState 钩子来设置初始状态。

让我们来看一个例子:

export default function App() {
  const [name] = React.useState("Mike");
  const [age] = React.useState(32);
  return (
    <>
      <p>My name is {name}</p>
      <p>My age is {age}</p>
    </>
  );
} 

App 组件是一个功能性的 React 组件,它返回 JSX 标记。但现在它也是一个有状态的组件,多亏了 useState 钩子。这个例子初始化了两个状态值,nameage。这就是为什么有两个 useState 调用,每个状态值一个。

你可以在组件中拥有你需要的任意数量的状态。最佳实践是每个状态值使用一个 useState 调用。你当然可以使用一个 useState 调用来定义一个对象作为组件的状态,但这会使事情变得复杂,因为你必须通过对象来访问状态值,而不是直接访问。使用这种方法更新状态值也会更复杂。如果有疑问,请为每个状态值使用一个 useState 钩子。

当我们调用 useState 时,我们得到一个返回给我们的数组。这个数组的第一个值是状态值本身。由于我们在这里使用了数组解构语法,我们可以将值命名为我们想要的任何名称;在这种情况下,它是 nameage。这两个常量在组件首次渲染时都有值,因为我们已经将每个的初始状态值传递给了 useState。以下是渲染后的页面外观:

图 3.1:使用状态钩子值渲染的输出

既然你已经看到了如何设置组件的初始状态值,让我们来了解一下如何更新这些值。

更新状态值

React 组件使用状态来表示随时间变化的价值。组件使用的状态值最初处于一种状态,就像我们在上一节中看到的那样,然后响应某些事件而改变:例如,服务器响应 API 请求并返回新数据,或者用户点击了按钮或更改了表单字段。

要更新状态,useState 钩子为每一块状态提供了一个单独的函数,我们可以从 useState 钩子返回的数组中访问它。第一个元素是状态值,第二个是用于更新值的函数。让我们看看一个例子:

function App() {
  const [name, setName] = React.useState("Mike");
  const [age, setAge] = React.useState(32);
  return (
    <>
      <section>
        <input value={name} onChange={(e) => setName(e.target.value)} />
        <p>My name is {name}</p>
      </section>
      <section>
        <input
          type="number"
          value={age}
          onChange={(e) => setAge(e.target.value)}
        />
        <p>My age is {age}</p>
      </section>
    </>
  );
} 

就像初始状态值部分中的示例一样,本例中的 App 组件有两个状态:nameage。与前面的示例不同,此组件使用两个函数来更新每一块状态。这些函数是从 useState 调用中返回的。让我们更仔细地看看:

const [name, setName] = React.useState("Mike");
const [age, setAge] = React.useState(32); 

现在,我们有两个函数:setNamesetAge,它们可以用来更新我们组件的状态。让我们看看更新 name 状态的文本输入字段:

<section>
  <input value={name} onChange={(e) => setName(e.target.value)} />
  <p>My name is {name}</p>
</section> 

当用户更改 <input> 字段中的文本时,会触发 onChange 事件。此事件的处理器调用 setName,并将 e.target.value 作为参数传递给它。传递给 setName 的参数是名称的新状态值。接下来的段落显示了每次用户更改文本输入时,文本输入也会更新名称的新值。

接下来,让我们看看 age 数字输入字段以及这个值是如何传递给 setAge 的:

<section>
  <input
    type="number"
    value={age}
    onChange={(e) => setAge(e.target.value)}
  />
  <p>My age is {age}</p>
</section> 

age 字段遵循与 name 字段完全相同的模式。唯一的区别是我们将输入类型设置为数字。每当数字发生变化时,setAge 就会被调用,并带有 onChange 事件响应的更新值。接下来的段落显示了随着 age 状态的每次更改,数字输入也会更新。

当这两个输入及其对应的段落在屏幕上渲染时,它们看起来是这样的:

图 3.2:使用 Hooks 更改状态值

在本节中,你学习了 useState 钩子,它用于向功能 React 组件添加状态。每一块状态都使用它自己的钩子,并有自己的值变量和自己的设置函数。这大大简化了在组件中访问和更新状态。任何给定的状态值都应该有一个初始值,这样组件才能在第一次渲染时正确显示。要重新渲染使用状态钩子的功能组件,你可以使用 useState 返回的设置函数来按需更新你的状态值。

你接下来要学习的下一个钩子用于执行初始化和清理操作。

执行初始化和清理操作

通常,我们的 React 组件需要在组件创建时执行某些操作。例如,一个常见的初始化操作是获取组件需要的 API 数据。另一个常见操作是在组件移除时确保任何挂起的 API 请求被取消。在本节中,你将了解 useEffect 钩子以及它如何帮助你在这些两种场景下。你还将了解如何确保初始化代码不会运行得太频繁。

获取组件数据

useEffect 钩子用于在组件中运行“副作用”。另一种思考副作用代码的方式是,函数组件只有一个任务:返回用于渲染的 JSX 内容。如果组件需要做其他事情,例如获取 API 数据,这应该在 useEffect 钩子中完成。例如,如果你只是将 API 调用作为组件函数的一部分,你可能会引入竞态条件和其他难以修复的故障行为。

让我们看看一个使用 Hooks 获取 API 数据的示例:

function App() {
  const [id, setId] = React.useState("loading...");
  const [name, setName] = React.useState("loading...");
  const fetchUser = React.useCallback(() => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ id: 1, name: "Mike" });
      }, 1000);
    });
  }, []);
  React.useEffect(() => {
    fetchUser().then((user) => {
      setId(user.id);
      setName(user.name);
    });
  });
  return (
    <>
      <p>ID: {id}</p>
      <p>Name: {name}</p>
    </>
  );
} 

useEffect 钩子期望一个函数作为参数。这个函数在组件完成渲染后以安全的方式被调用,不会干扰 React 在组件背后进行的任何其他操作。让我们更仔细地看看这个示例的各个部分,从模拟 API 函数开始:

const fetchUser = React.useCallback(() => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "Mike" });
    }, 1000);
  });
}, []); 

fetchUser 函数使用 useCallback 钩子定义。这个钩子用于记忆函数,意味着它只会创建一次,并且除非依赖项发生变化,否则不会在后续渲染中重新创建。useCallback 接受两个参数:第一个是我们想要记忆的函数,第二个是用于识别 React 应该重新创建此函数而不是使用记忆版本的依赖项列表。fetchUser 函数将空数组 ([]) 作为依赖项列表传递。这意味着函数在初始渲染期间只会创建一次,并且在后续渲染中不会重新创建。

fetchUser 函数返回一个承诺。承诺解析一个包含两个属性 idname 的简单对象。setTimeout 函数将承诺解决延迟 1 秒,因此这个函数是异步的,就像正常的 fetch 调用一样。

接下来,让我们看看 App 组件使用的 Hooks:

const [id, setId] = React.useState("loading...");
const [name, setName] = React.useState("loading...");
React.useEffect(() => {
  fetchUser().then((user) => {
    setId(user.id);
    setName(user.name);
  });
}); 

如您所见,除了 useCallback,我们还在这个组件中使用了两个 Hooks:useStateuseEffect。以这种方式组合 hook 功能非常强大且被鼓励。首先,我们设置组件的 idname 状态。然后,使用 useEffect 设置一个函数,当承诺解决时调用 fetchUser 并设置组件的状态。

这是 App 组件首次渲染时的样子,使用初始状态 idname

图 3.3:显示加载文本,直到数据到达

图 3.3:在数据到达之前显示加载文本

1 秒后,fetchUser 返回的 promise 使用 API 数据解析,然后用于更新 ID 和名称状态。这导致 App 重新渲染:

图片

图 3.4:状态变化,移除加载文本并显示返回值

有很大可能性,你的用户会在 API 请求挂起时在应用中导航。useEffect 钩子可以用来处理取消这些请求。

取消操作和重置状态

有很大可能性,在某个时刻,你的用户会在 API 请求的响应到达之前导航你的应用,导致组件卸载。有时你的组件可以监听某些事件,你应该在卸载组件之前删除所有监听器以避免内存泄漏。一般来说,当相关组件从屏幕中删除时,停止执行任何后台操作是很重要的。

幸运的是,useEffect 钩子有一个机制来清理组件移除时挂起的 setInterval 等效果。让我们看看一个实际应用的例子:

import * as React from "react";
function Timer() {
  const [timer, setTimer] = React.useState(100);
  React.useEffect(() => {
    const interval = setInterval(() => {
      setTimer((prevTimer) => (prevTimer === 0 ? 0 : prevTimer - 1));
    }, 1000);
    return () => {
      clearInterval(interval);
    };
  }, []);
  return <p>Timer: {timer}</p>;
}
export default Timer; 

这是一个简单的 Timer 组件。它具有 timer 状态,它在 useEffect() 中设置间隔回调以更新 timer,并使用当前 timer 值渲染输出。让我们更仔细地看看 useEffect() 钩子:

React.useEffect(() => {
  const interval = setInterval(() => {
    setTimer((prevTimer) => (prevTimer === 0 ? 0 : prevTimer - 1));
  }, 1000);
  return () => {
    clearInterval(interval);
  };
}, []); 

此效果通过调用带有回调的 setInterval 函数创建一个间隔计时器,该回调更新我们的 timer 状态。你在这里会发现有趣的是,对于 setTimer 函数,我们传递的是一个回调而不是一个数字。这是一个有效的 React API:当我们需要使用前一个状态值来计算新值时,我们可以传递一个回调,其中第一个参数是当前或“前一个”状态值,我们应该从这个回调中返回新状态值以更新我们的状态。

useEffect 中,我们还在返回一个函数,React 在组件移除时运行此函数。在这个例子中,通过调用 setInterval 创建的间隔被调用 useEffect 中返回的函数的 clearInterval 清除。从 useEffect 返回的函数将在组件将要卸载时触发。

现在,让我们看看渲染和移除 Timer 组件的 App 组件:

const ShowHideTimer = ({ show }) => (show ? <Timer /> : null);
function App() {
  const [show, setShow] = React.useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? "Hide Timer" : "Show Timer"}
      </button>
      <ShowHideTimer show={show} />
    </>
  );
} 

App 组件渲染一个按钮,用于切换 show 状态。此状态值决定是否渲染 Timer 组件,但通过使用 ShowHideTimer 便利组件。如果 show 为真,则渲染 <Timer />;否则,移除 Timer,触发我们的 useEffect 清理行为。

这是屏幕首次加载时的样子:

图片

图 3.5:用于启动状态变化的按钮

由于App组件的show状态为falseTimer组件没有被渲染。尝试点击显示计时器按钮。这将改变show状态并渲染Timer组件:

图片

图 3.6:显示计时器

你可以再次点击隐藏计时器按钮来移除Timer组件。如果没有添加到useEffect中的清理间隔,这将每次计时器渲染时创建新的监听器,这将影响内存泄漏。

React 允许我们控制何时运行我们的效果。例如,当我们想在第一次渲染后进行所有 API 请求,或者当特定状态改变时执行效果。我们将看看如何做到这一点。

优化副作用操作

默认情况下,React 假设每个运行的效果都需要清理,并且应该在每次渲染时运行。这通常不是情况。例如,你可能有一些特定的属性或状态值需要清理,并在它们改变时再运行一次。你可以将一个要观察的值的数组作为useEffect的第二个参数传递:例如,如果你有一个在改变时需要清理的解析状态,你会这样编写你的效果代码:

const [resolved, setResolved] = useState(false);
useEffect(() => {
  // ...the effect code...
  return () => {
    // ...the cleanup code
  };
}, [resolved]); 

在此代码中,效果将在状态值解析改变时触发,并且只有在解析状态值改变时才会运行。如果效果运行但解析状态没有改变,则清理代码将不会运行,原始效果代码也不会再次运行。另一个常见的情况是,除了组件被移除时,从不运行清理代码。实际上,这正是我们在“获取用户数据”部分示例中想要发生的。目前,效果在每次渲染后都会运行。这意味着我们反复获取用户 API 数据,而我们真正想要的只是当组件首次挂载时获取一次。

让我们对从获取组件数据请求示例中的App组件进行一些修改:

React.useEffect(() => {
  fetchUser().then((user) => {
    setId(user.id);
    setName(user.name);
  });
}, []); 

我们向useEffect添加了一个第二个参数,一个空数组。这告诉 React 没有要观察的值,并且我们只想在渲染后运行效果,并在组件移除时运行清理代码。我们还向fetchUser函数中添加了console.count('fetching user')。这使得查看浏览器开发者工具控制台并确保我们的组件数据只获取一次变得更容易。如果你从传递给useEffect[]参数中移除,你会注意到fetchUser被多次调用。

在本节中,你了解了 React 组件中的副作用。效果是一个重要的概念,因为它们是 React 组件和外部世界之间的桥梁。效果最常见的使用场景之一是在组件首次创建时获取组件所需的数据,然后在组件移除后进行清理。

现在,我们将探讨另一种与 React 组件共享数据的方法:上下文。

使用上下文 Hooks 共享数据

React 应用程序通常有一些全局性质的数据。这意味着几个组件,可能是一个应用程序中的所有组件,共享这些数据:例如,当前登录用户的信息可能在多个地方使用。这就是Context API派上用场的地方。Context API 提供了一种创建共享数据存储的方法,任何树中的组件都可以访问,无论其深度如何。

要利用 Context API,我们需要使用React库中的createContext函数创建一个上下文:

import { createContext } from 'react';
const MyContext = createContext(); 

在上面的例子中,我们使用createContext创建了一个名为MyContext的上下文。这创建了一个包含ProviderConsumer的上下文对象。

Provider组件负责将共享数据提供给其子组件。我们用Provider包装组件树的相关部分,并通过value属性传递数据:

<MyContext.Provider value={/* shared data */}>
  {/* Child components */}
</MyContext.Provider> 

MyContext.Provider内的任何组件都可以使用Consumer组件或useContext钩子访问共享数据。让我们看看如何使用钩子读取上下文:

import React, { useContext } from 'react';
const MyComponent = () => {
  const value = useContext(MyContext);
  // Render using the shared data
}; 

通过利用 Context API,我们可以避免需要通过多个组件层级传递数据的 prop-drilling 问题。它简化了数据共享的过程,并允许组件直接访问共享数据,使代码更易于阅读和维护。

值得注意的是,Context API 并不适用于所有场景,应谨慎使用。它对于共享真正全局或与组件树大部分相关联的数据最有用。对于较小规模的数据共享,属性仍然是推荐的方法。

使用 Hooks 进行缓存

在 React 中,函数组件在每次渲染时都会被调用,这意味着昂贵的计算和函数创建可能会对性能产生负面影响。为了优化性能并防止不必要的重新计算,React 提供了三个 Hooks:useMemouseCallbackuseRef。这些 Hooks 允许我们分别缓存值、函数和引用。

useMemo 钩子

useMemo钩子用于缓存计算结果,确保只有当依赖项发生变化时才重新计算。它接受一个函数和一个依赖项数组,并返回缓存的值。

下面是使用useMemo钩子的一个例子:

import { useMemo } from 'react';
const Component = () => {
  const expensiveResult = useMemo(() => {
    // Expensive computation
    return computeExpensiveValue(dependency);
  }, [dependency]);
  return <div>{expensiveResult}</div>;
}; 

在这个例子中,expensiveResult值使用useMemo进行了缓存。函数内的计算只有在dependency值改变时才会执行。如果dependency保持不变,则返回之前缓存的值,而不是重新计算结果。

useCallback 钩子

我们已经在本章中探讨了 useCallback 钩子,但我想要强调一个重要的用例。当一个函数组件渲染时,它所有的函数都会被重新创建,包括在组件内部定义的任何内联回调。这可能导致接收这些回调作为属性的孩子组件的不必要重新渲染,因为这些组件将回调视为新的引用并触发重新渲染。让我们看看下面的例子:

const MyComponent = () => {
  return <MyButton onClick={() => console.log("click")} />;
}; 

在这个例子中,我们提供给 onClick 属性的匿名函数将在 MyComponent 每次渲染时创建。这意味着 MyButton 组件每次都会接收到一个新的函数引用,正如我们已知的,这将导致 MyButton 组件的新渲染。

下面是一个演示 useCallback 钩子使用的例子:

const MyComponent = () => {
  const clickHandler = React.useCallback(() => {
    console.log("click");
  }, []);
  return <MyButton onClick={clickHandler} />;
}; 

在这个例子中,clickHandler 函数是通过 useCallback 进行缓存的。空依赖数组 [] 表示该函数没有依赖项,并且应该在组件的生命周期内保持不变。

因此,在 MyComponent 的每次渲染中,都会向 MyButton 提供相同的函数实例,从而防止孩子组件的不必要重新渲染。

useRef 钩子

useRef 钩子允许我们创建一个在组件渲染之间持续存在的可变引用。它通常用于存储需要在渲染之间保留的值或引用,而不会触发重新渲染。此外,useRef 可以用来访问 DOM 节点或 React 组件实例:

const Component = () => {
  const inputRef = useRef();
  const handleButtonClick = () => {
    inputRef.current.focus();
  };
  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleButtonClick}>Focus Input</button>
    </div>
  );
}; 

在这个例子中,inputRef 是通过 useRef 创建的,并且被分配给了 input 元素的 ref 属性。这使我们能够通过 inputRef.current 属性访问 DOM 节点。在 handleButtonClick 函数中,我们调用 inputRef.current 上的 focus 方法,以便在按钮被点击时聚焦输入元素。

通过使用 useRef 访问 DOM 节点,我们可以直接与底层的 DOM 元素交互,而不会触发组件的重新渲染。

通过利用 useMemouseCallbackuseRef 钩子的缓存功能,我们可以通过避免不必要的计算、防止不必要的重新渲染以及跨渲染保留值和引用来优化我们的 React 应用程序的性能。这导致更平滑的用户体验和更高效地使用资源。

摘要

本章向您介绍了 React 组件和 React Hooks。您通过实现将属性值从 JSX 传递到组件的代码来学习了组件属性或 props。接下来,您了解了状态是什么以及如何使用 useState 钩子来操作它。然后,您学习了 useEffect,它使功能 React 组件能够进行生命周期管理,例如在组件挂载时获取 API 数据,以及在组件移除时清理任何挂起的异步操作。然后,您学习了如何使用 useContext() 钩子来访问全局应用程序数据。最后,您学习了使用 useMemouseCallbackuseMemo 钩子进行记忆化。

在下一章中,您将学习如何使用 React 组件处理事件。

第四章:React 方式的事件处理

本章的重点是高阶事件处理器函数。

之后,你将学习 React 如何在底层将事件处理器映射到 DOM 元素。最后,你将了解 React 传递给事件处理器函数的合成事件以及它们如何为了性能目的而被池化。一旦你完成这一章,你将能够轻松地在你的 React 组件中实现事件处理器。到那时,你的应用程序将因为用户能够与之交互而变得生动起来。

本章涵盖了以下主题:

  • 声明事件处理器

  • 声明内联事件处理器

  • 将处理器绑定到元素上

  • 使用合成事件对象

  • 理解事件池化

技术要求

本章中展示的代码可以在以下链接找到:github.com/PacktPublishing/React-and-React-Native-5E/tree/main/Chapter04

声明事件处理器

与 React 组件中的事件处理相比,其区分因素在于它是声明式的。将此与例如jQuery进行比较,在 jQuery 中,你必须编写命令式代码来选择相关的 DOM 元素并将事件处理器函数附加到它们上。

JSX 标记中声明式事件处理器方法的优点是它们是 UI 结构的一部分。不必追踪分配事件处理器的代码,这在心理上是一种解放。

在本节中,你将编写一个基本的事件处理器,以便你可以感受到在 React 应用程序中找到的声明式事件处理语法。然后,你将学习如何使用通用事件处理器函数。

声明处理器函数

让我们看看一个基本的组件,它声明了一个元素的点击事件处理器:

function MyButton(props) {
  const clickHandler = () => {
    console.log("clicked");
  };
  return <button onClick={clickHandler}>{props.children}</button>;
} 

clickHandler函数被传递到<button>元素的onClick属性。通过查看这个标记,你可以确切地看到当按钮被点击时将运行哪些代码。

react.dev/reference/react-dom/components/common查看官方 React 文档,以获取支持的完整事件属性名称列表。

接下来,让我们看看如何使用同一元素的不同事件处理器来响应多种类型的事件。

多个事件处理器

我非常喜欢声明式事件处理器语法的是,当分配给一个元素多个处理器时,它很容易阅读。有时,例如,一个元素有两个或三个处理器。对于单个事件处理器,命令式代码就很难处理,更不用说多个处理器了。当一个元素需要更多处理器时,它只是另一个 JSX 属性。从代码可维护性的角度来看,这一点从以下示例中可以看出:

function MyInput() {
  const onChange = () => {
    console.log("changed");
  };
  const onBlur = () => {
    console.log("blured");
  };
  return <input onChange={onChange} onBlur={onBlur} />;
} 

这个<input>元素可以有更多的事件处理器,代码的可读性仍然很好。

当你继续向你的组件添加更多的事件处理器时,你会注意到其中很多都在做同样的事情。接下来,你将学习关于内联事件处理器函数的内容。

声明内联事件处理器

将处理函数分配给 JSX 属性的传统方法是使用命名函数。然而,有时你可能想使用内联函数,其中函数作为标记的一部分被定义。这是通过将箭头函数直接分配给 JSX 标记中的事件属性来完成的:

function MyButton(props) {
  return (
    <button onClick={(e) => console.log("clicked", e)}>
      {props.children}
    </button>
  );
} 

使用这种内联事件处理器的最主要用途是当你有一个想要传递给另一个函数的静态参数值时。在这个例子中,你正在调用console.log并传入点击的字符串。你可以通过创建一个新的函数或使用高阶函数,在 JSX 标记之外设置一个特殊函数来达到这个目的。但那样你就需要为另一个函数想一个新的名字。有时候内联处理器的使用会更简单一些。

接下来,你将学习 React 是如何将处理函数绑定到浏览器中底层的 DOM 元素上的。

将处理器绑定到元素上

当你在 JSX 中将事件处理器函数分配给一个元素时,React 实际上并没有将事件监听器附加到底层的 DOM 元素上。相反,它将函数添加到内部函数映射中。页面上有一个文档级别的单一事件监听器。当事件通过 DOM 树向上冒泡到文档时,React 处理器会检查是否有任何组件具有匹配的处理器。这个过程在这里被展示出来:

图 4.1:事件处理器周期

你可能会问,为什么 React 要费这么大的劲?这与我在前几章中一直在讲述的相同原则:尽可能地将声明性 UI 结构从 DOM 中分离出来。DOM 仅仅是一个渲染目标;React 的架构允许它对最终的渲染目的地和事件系统保持中立。

例如,当一个新组件被渲染时,它的处理函数被简单地添加到 React 维护的内部映射中。当一个事件被触发并击中文档对象时,React 将事件映射到处理器。如果找到匹配项,它将调用处理器。最后,当React 组件被移除时,处理器将从处理器列表中简单地移除。

这些 DOM 操作实际上并没有触及 DOM。这一切都被一个单独的事件监听器抽象了。这对性能和整体架构(换句话说,保持渲染目标与应用代码的分离)是有好处的。

在接下来的部分,你将学习 React 如何使用合成事件实现来确保良好的性能和安全的异步行为。

使用合成事件对象

当您使用原生的 addEventListener 函数将事件处理函数附加到 DOM 元素时,回调将获得一个事件参数传递给它。React 中的事件处理函数也传递一个事件参数,但它不是标准的事件实例。它被称为 SyntheticEvent,它是原生事件实例的简单包装。

合成事件在 React 中具有两个目的:

  • 它们提供了一个一致的事件接口,标准化了浏览器的不一致性。

  • 它们包含传播所需的信息。

这里是一个React 组件上下文中合成事件的图示:

图 4.2:合成事件是如何创建和处理的

当一个 DOM 元素作为React 组件的一部分派发事件时,React 将处理该事件,因为它为它们设置了自有的监听器。然后,根据可用性,它将创建一个新的合成事件或从池中重用其中一个。如果为该组件声明了任何与派发的 DOM 事件匹配的事件处理程序,它们将使用传递给它们的合成事件运行。

React 中的事件对象具有与原生 JavaScript 事件类似的属性和方法。您可以通过 event.target 访问属性来检索触发事件的 DOM 元素,或通过 event.currentTarget 来引用事件处理程序附加到的元素。

此外,事件对象提供了如 event.preventDefault() 这样的方法来阻止与事件关联的默认行为,例如表单提交或链接点击。您还可以使用 event.stopPropagation() 来阻止事件进一步向上传播到组件树,防止事件冒泡。

事件传播在 React 中与传统 JavaScript 事件处理不同。在传统方法中,事件通常通过 DOM 树向上冒泡,触发祖先元素上的处理程序。

在 React 中,事件传播基于组件层次结构而不是 DOM 层次结构。当一个事件在子组件中发生时,React 会在组件树的根处捕获该事件,然后向下遍历到触发事件的特定组件。这种方法称为事件委托,通过在组件树的根处集中事件逻辑来简化事件处理。

React 的事件委托提供了几个好处。首先,它减少了附加到单个 DOM 元素上的事件监听器的数量,从而提高了性能。其次,它允许您处理动态创建或删除的元素的事件,而无需担心手动附加或移除事件监听器。

在下一节中,您将看到这些合成事件如何为了性能原因而被池化,以及这对异步代码的影响。

理解事件池化

将原生事件实例包装起来的一大挑战是它可能会引起性能问题。每个创建的合成事件包装器最终都需要进行垃圾收集,这在 CPU 时间上可能代价高昂。

当垃圾收集器运行时,你的任何 JavaScript 代码都无法运行。这就是为什么内存效率很重要;频繁的垃圾收集意味着响应用户交互的代码有更少的 CPU 时间。

例如,如果你的应用程序只处理少量事件,这不会有多大影响。但即使按照适度标准,应用程序也会响应许多事件,即使处理程序实际上并没有对它们做任何事情。如果 React 不断需要分配新的合成事件实例,这就会成为问题。

React 通过分配一个合成实例池来处理这个问题。每当一个事件被触发时,它会从池中取出一个实例并填充其属性。当事件处理程序运行完成后,合成事件实例被释放回池中,如下所示:

图 4.3:合成事件被重复使用以节省内存资源

这防止了在触发大量事件时垃圾收集器频繁运行。池保留了合成事件实例的引用,因此它们永远不会符合垃圾收集的条件。React 也永远不会需要分配新实例。

然而,有一个需要注意的陷阱。它涉及到从事件处理程序中的异步代码访问合成事件实例。这是一个问题,因为一旦处理程序运行完成,实例就会回到池中。当它回到池中时,所有属性都会被清除。

下面是一个示例,说明这可能会出错:

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 1000);
  });
}
function MyButton(props) {
  function onClick(e) {
    console.log("clicked", e.currentTarget.style);
    fetchData().then(() => {
      console.log("callback", e.currentTarget.style);
    });
  }
  return <button onClick={onClick}>{props.children}</button>;
} 

第二次调用 console.log 尝试从一个异步回调中访问合成事件属性,该回调直到事件处理程序完成才运行,这导致事件清空了其属性。这会产生一个警告和一个未定义的值。

本例的目的是说明当你编写与事件交互的异步代码时,事情可能会出错。只是不要这样做!

在本节中,你了解到事件被池化是为了性能原因,这意味着你永远不应该以异步方式访问事件对象。

摘要

本章介绍了 React 中的事件处理。React 与其他事件处理方法的关键区别在于处理程序是在 JSX 标记中声明的。这使得追踪哪些元素处理哪些事件变得简单得多。

你了解到在单个元素上添加多个事件处理程序只是添加新的 JSX 属性的问题。然后,你学习了内联事件处理函数及其潜在用途,以及 React 实际上如何将单个 DOM 事件处理程序绑定到文档对象。

合成事件是封装原生事件的抽象;你已经了解到它们为何是必要的,以及它们是如何为了高效内存消耗而被池化的。

在下一章中,你将学习如何创建适用于多种目的的可重用组件。而不是为每个遇到的使用案例编写新的组件,你将学会必要的技能来重构现有组件,以便它们可以在多个上下文中使用。

第五章:构建可复用组件

本章的目标是向您展示如何实现具有多个目的的 React 组件。阅读本章后,您将对自己的应用功能组合充满信心。

本章首先简要介绍了 HTML 元素及其在帮助实现功能与具有高度实用性方面的作用。然后,您将看到单一组件的实现,并发现它将带来的问题。下一节将致力于以这种方式重新实现单一组件,即功能由更小的组件组成。

最后,本章以对 React 组件渲染树的讨论结束,并为您提供一些关于如何避免在分解组件时引入过多复杂性的建议。我将在最后一节重申高级功能组件与实用组件的概念。

本章将涵盖以下主题:

  • 可复用 HTML 元素

  • 单一组件的困难之处

  • 重构组件结构

  • 渲染属性

  • 渲染组件树

技术要求

您可以在 GitHub 上找到本章的代码文件,链接为github.com/PacktPublishing/React-and-React-Native-5E/tree/main/Chapter05

可复用 HTML 元素

让我们暂时思考一下 HTML 元素。根据 HTML 元素的类型,它要么是功能中心的,要么是实用中心的。实用中心的 HTML 元素比功能中心的 HTML 元素更易于复用。例如,考虑<section>元素。这是一个通用元素,几乎可以用于任何地方,但其主要目的是组成功能的结构方面:功能的壳体和功能的内部部分。这就是<section>元素最有用的地方。

在另一边,您有像<p><span><button>这样的元素。这些元素因为设计上的通用性而提供了高级的实用性。当用户可以点击并执行操作时,您应该使用<button>元素,这比功能的概念低一个层次。

虽然谈论具有高度实用性的 HTML 元素与针对特定功能设计的元素很容易,但当涉及数据时,讨论会更加详细。HTML 是静态标记;React 组件将静态标记与数据相结合。问题是,您如何确保您正在创建正确的以功能为中心和以实用为中心的组件?

本章的目标是找出如何将定义功能的单一 React 组件转变为更小的以功能为中心的组件,并结合实用组件。

单一组件的困难之处

如果你只为任何给定的功能实现一个组件,这将简化你的工作。至少,维护的组件不会很多,数据的流动路径也不会很多,因为所有内容都在组件内部。

然而,这个想法由于多种原因而不可行。拥有单体功能组件使得协调任何类型的团队开发工作变得困难,例如版本控制合并冲突并行开发。单体组件越大,将来重构为更好的组件就越困难。

还有一个功能重叠和功能通信的问题。重叠是由于功能之间的相似性造成的;一个应用程序不太可能有一组完全独特的功能。这将使应用程序非常难以学习和使用。组件通信基本上意味着一个功能中某个东西的状态将影响另一个功能中某个东西的状态。状态很难处理,当有很多状态被封装在单体组件中时,情况更是如此。

学习如何避免单体组件的最佳方式是亲身体验。你将在本节的剩余部分实现一个单体组件。在下一节中,你将看到这个组件是如何被重构为更可持续的样式的。

JSX 标记

我们将要实现的单体组件是一个列出文章的功能。这只是为了说明目的,所以我们不想让组件的大小过大。它将简单而单体。用户可以向列表中添加新项目,切换列表中项目的摘要,并从列表中删除项目。

这是组件的 JSX 标记:

<section>
      <header>
        <h1>Articles</h1>
        <input placeholder="Title" value={title} onChange={onChangeTitle} />
        <input
          placeholder="Summary"
          value={summary}
          onChange={onChangeSummary}
        />
        <button onClick={onClickAdd}>Add</button>
      </header>
      <article>
        <ul>
          {articles.map((i) => (
            <li key={i.id}>
              <a
                href={'#${i.id}'}
                title="Toggle Summary"
                onClick={() => onClickToggle(i.id)}
              >
                {i.title}
              </a>
              &nbsp;
              <button
                href={'#${i.id}'}
                title="Remove"
                onClick={() => onClickRemove(i.id)}
              >
                &#10007;
              </button>
              <p style={{ display: i.display }}>{i.summary}</p>
            </li>
          ))}
        </ul>
      </article>
    </section> 

这里的 JSX 明显比必要的多。我们将在下一节中改进这一点,但现在,让我们为这个组件实现初始状态。

初始状态

现在,让我们看看这个组件的初始状态:

 const [articles, setArticles] = React.useState([
    {
      id: id.next(),
      title: "Article 1",
      summary: "Article 1 Summary",
      display: "none",
    },
    {
      id: id.next(),
      title: "Article 2",
      summary: "Article 2 Summary",
      display: "none",
    },
  ]);
  const [title, setTitle] = React.useState("");
  const [summary, setSummary] = React.useState(""); 

状态由一个articles数组、一个title字符串和一个summary字符串组成。articles数组中的每个文章对象都有几个字符串字段来帮助渲染文章,以及一个id字段,这是一个数字。这个数字是由id.next()生成的。

让我们看看这是如何工作的:

const id = (function* () {
  let i = 1;
  while (true) {
    yield i;
    i += 1;
  }
})(); 

id 常量是一个生成器。它通过定义一个内联生成器函数并立即调用它来创建。这个生成器将无限期地产生数字。所以,第一次调用id.next()返回1,下一次是2,依此类推。这个简单的实用工具在需要添加新文章并需要一个新唯一 ID 时会很有用。

事件处理器实现

到目前为止,你已经有了组件的初始状态和 JSX 代码。现在,是时候实现事件处理器了:

 const onChangeTitle = useCallback((e) => {
    setTitle(e.target.value);
  }, []);
  const onChangeSummary = useCallback((e) => {
    setSummary(e.target.value);
  }, []); 

onChangeTitle()onChangeSummary()方法分别使用钩子的setState()来更新titlesummary状态值。新值来自event参数的target.value属性,这是用户输入到文本输入中的值:

 const onClickAdd = useCallback(() => {
    setArticles((state) => [
      ...state,
      {
        id: id.next(),
        title: title,
        summary: summary,
        display: "none",
      },
    ]);
    setTitle("");
    setSummary("");
  }, [summary, title]); 

onClickAdd()方法向articles状态添加一篇新文章。这个状态值是一个数组。我们使用扩展运算符从现有数组([...state])构建一个新数组,并将新对象添加到新数组的末尾。我们构建新数组并将其传递给setArticles()的原因是为了避免意外。换句话说,我们将状态值视为不可变,以便更新相同状态的其它代码不会意外地造成问题。接下来,我们将使用一个处理程序来删除文章:

 const onClickRemove = useCallback((id) => {
    setArticles((state) => 
      state.filter((article) => article.id !== id)
    );
  }, []); 

onClickRemove()方法从articles状态中删除具有给定 ID 的文章。它是通过在数组上调用filter()来实现的,这返回一个新数组,因此操作是不可变的。过滤器删除具有给定 ID 的对象:

 const onClickToggle = useCallback((id) => {
    setArticles((state) => {
      const articles = [...state];
      const index = articles.findIndex((article) => article.id === id);
      articles[index] = {
        ...articles[index],
        display: articles[index].display ? "" : "none",
      };
      return articles;
    });
  }, []); 

onClickToggle()方法切换具有给定 ID 的文章的可见性。在这个方法中,我们执行了两个不可变操作。首先,我们构建一个新的articles数组。然后,根据给定 ID 的索引,我们用新对象替换索引处的文章对象。我们使用对象扩展运算符来填充属性({...articles[index]}),然后根据现有的显示值切换显示属性值。

这里是输出渲染的截图:

图 5.1:渲染的文章

到目前为止,我们有一个组件,它完成了我们需要的所有功能。然而,它是单体化的,难以维护。想象一下,如果我们应用中的其他地方也使用了相同的MyFeature片段。他们必须重新发明它们,因为他们无法共享。在下一节中,我们将努力将MyFeature分解为更小的可重用组件。

重构组件结构

你有一个单体功能组件:接下来怎么办?让我们让它变得更好。

在本节中,你将学习如何将上一节中实现的功能组件拆分为更易于维护的组件。你将从JSX开始,因为这可能是最佳的重构起点。然后,你将为功能实现新的组件。

接下来,你将使这些新组件具有功能性,而不是基于类的。最后,你将学习如何使用渲染属性来减少应用程序中直接组件依赖项的数量,以及如何通过在功能组件中使用钩子来管理状态来完全删除类。

从 JSX 开始

任何单体组件JSX是将其重构为更小组件的最佳起点。让我们可视化我们目前正在重构的组件结构:

图 5.2:构成 React 组件的 JSX 可视化

JSX 的上半部分是表单控件,因此这可以很容易地成为一个自己的组件:

<header>
  <h1>Articles</h1>
  <input 
    placeholder="Title" 
    value={title} 
    onChange={onChangeTitle} />
  <input 
    placeholder="Summary" 
    value={summary} 
    onChange={onChangeSummary} />
  <button onClick={onClickAdd}>Add</button>
</header>; 

接下来,你有文章列表:

<ul>
  {articles.map((i) => (
    <li key={i.id}>
      <a
        href={`#${i.id}`}
        title="Toggle Summary"
        onClick={() => onClickToggle(i.id)}
      >
        {i.title}
      </a>
      &nbsp;
      <button
        href={'#${i.id}'}
        title="Remove"
        onClick={() => onClickRemove(i.id)}
      >
        &#10007;
      </button>
      <p style={{ display: i.display }}>{i.summary}</p>
    </li>
  ))}
</ul> 

在这个列表中,有一个文章组件的潜力,它包括了 <li> 标签中的所有内容。让我们尝试构建这个组件。

实现文章列表组件

这是 ArticleList 组件的实现看起来像:

function ArticleList({ articles, onClickToggle, onClickRemove }) {
  return (
    <ul>
      {articles.map((i) => (
        <li key={i.id}>
          <a
            href={'#${i.id}'}
            title="Toggle Summary"
            onClick={() => onClickToggle(i.id)}
          >
            {i.title}
          </a>
          &nbsp;
          <button
            href={'#${i.id}'}
            title="Remove"
            onClick={() => onClickRemove(i.id)}
          >
            &#10007;
          </button>
          <p style={{ display: i.display }}>{i.summary}</p>
        </li>
      ))}
    </ul>
  );
} 

我们将相关的 JSX 从单体组件中提取出来,并将其放在这里。现在,让我们看看功能组件的 JSX 看起来像:

 <section>
      <header>
        <h1>Articles</h1>
        <input placeholder="Title" value={title} onChange={onChangeTitle} />
        <input
          placeholder="Summary"
          value={summary}
          onChange={onChangeSummary}
        />
        <button onClick={onClickAdd}>Add</button>
      </header>
      <ArticleList
        articles={articles}
        onClickRemove={onClickRemove}
        onClickToggle={onClickToggle}
      />
    </section> 

文章列表现在由 ArticleList 组件渲染。要渲染的文章列表作为属性传递给此组件,同时还有两个事件处理程序。

为什么我们要将事件处理程序传递给子组件?原因是为了让 ArticleList 组件不必担心状态或状态如何变化。它只关心渲染内容和确保适当的事件回调被连接到适当的 DOM 元素。这是一个容器组件的概念,我将在本章后面进一步阐述。

现在我们有了 ArticleList 组件,让我们看看我们是否可以进一步将其分解成更小的可重用组件。

实现文章项目组件

在实现了文章列表组件之后,你可能会决定将其分解成更小的组件。

另一种看待它的方法是:如果最终我们发现实际上不需要将项目作为自己的组件,这个新组件不会引入太多的间接或复杂性。无需多言,以下是文章项目组件:

function ArticleItem({ article, onClickRemove }) {
  const [isOpened, setIsOpened] = React.useState(article.display !== "none");
  const onClickToggle = React.useCallback(() => {
    setIsOpened((state) => !state);
  }, []);
  return (
    <li>
      <a href={'#${article.id}'} title="Toggle Summary" onClick={onClickToggle}>
        {article.title}
      </a>
      &nbsp;
      <button
        href={'#${article.id}'}
        title="Remove"
        onClick={() => onClickRemove(article.id)}
      >
        &#10007;
      </button>
      <p style={{ display: isOpened ? "block" : "none" }}>{article.summary}</p>
    </li>
  );
} 

实际上,组件除了一个增强之外没有变化:我们将展开和折叠文章的逻辑重新定位到了 ArticleItem 组件,这提供了几个优点。首先,我们减少了原始的 MyFeature 组件,因为它根本不需要知道何时隐藏或展开文章。其次,由于在展开文章时,我们不再使用展开操作符重新创建文章数组,而是仅更改本地状态,因此我们提高了应用程序的性能。结果,在展开文章时,文章列表保持不变,React 不会重新渲染页面,但只有一个组件被重新渲染。

这是 ArticleList 组件正在渲染的新 ArticleItem 组件:

function ArticleList({ articles, onClickRemove }) {
  return (
    <ul>
      {articles.map((article) => (
        <ArticleItem
          key={article.id.value}
          article={article}
          onClickRemove={onClickRemove}
        />
      ))}
    </ul>
  );
} 

你看到这个列表是如何映射文章列表的吗?如果你想要实现另一个具有过滤功能的文章列表,那么拥有一个可重用的 ArticleItem 组件是有益的。接下来,我们将添加文章的标记移动到自己的组件中。

实现添加文章组件

现在我们完成了文章列表,是时候考虑用于添加新文章的表单控件了。让我们为这个功能方面实现一个组件:

function AddArticle({
  name,
  title,
  summary,
  onChangeTitle,
  onChangeSummary,
  onClickAdd,
}) {
  return (
    <section>
      <h1>{name}</h1>
      <input placeholder="Title" value={title} onChange={onChangeTitle} />
      <input placeholder="Summary" value={summary} onChange={onChangeSummary} />
      <button onClick={onClickAdd}>Add</button>
    </section>
  );
} 

现在,我们的功能组件只需要渲染 <AddArticle><ArticleList> 组件:

<section>
  <AddArticle
    name="Articles"
    title={title}
    summary={summary}
    onChangeTitle={onChangeTitle}
    onChangeSummary={onChangeSummary}
    onClickAdd={onClickAdd}
  />
  <ArticleList articles={articles} onClickRemove={onClickRemove} />
</section> 

这个组件的焦点在于功能数据,而它将渲染 UI 元素的任务委托给其他组件。在下一节中,我们将探讨渲染属性如何使得将组件作为属性传递而不是直接作为依赖项导入成为可能。

渲染属性

想象一下实现一个由几个较小的组件组成的功能,就像你在本章中一直在做的那样。MyFeature 组件依赖于 ArticleListAddArticle。现在,想象一下在不同的应用部分使用 MyFeature,在这些部分使用不同的 ArticleListAddArticle 实现是有意义的。基本挑战是替换一个组件为另一个组件。

渲染属性 是解决这个挑战的好方法。其思路是,你向你的组件传递一个属性,其值是一个返回要渲染的组件的函数。这样,你就可以配置它们,而不是让功能组件直接依赖于其子组件;它们将它们作为渲染属性值传递。让我们看看一个例子。与其让 MyFeature 直接依赖于 AddArticleArticleList,不如将它们作为渲染属性传递。以下是 MyFeature 使用渲染属性填充 add 之前所在空缺处的样子:

 <section>
      {addArticle({
        title,
        summary,
        onChangeTitle,
        onChangeSummary,
        onClickAdd,
      })}
      {articleList({ articles, onClickRemove })}
    </section> 

addArticle()articleList() 函数使用与 <AddArticle><ArticleList> 分别传递的相同属性值被调用。现在的不同之处在于,这个模块不再将 AddArticleArticleList 作为依赖项导入。

现在,让我们看看 <MyFeature> 被渲染的 main.js 文件:

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <MyFeature
    addArticle={({
      title,
      summary,
      onChangeTitle,
      onChangeSummary,
      onClickAdd,
    }) => (
      <AddArticle
        name="Articles"
        title={title}
        summary={summary}
        onChangeTitle={onChangeTitle}
        onChangeSummary={onChangeSummary}
        onClickAdd={onClickAdd}
      />
    )}
    articleList={({ articles, onClickRemove }) => (
      <ArticleList articles={articles} onClickRemove={onClickRemove} />
    )}
  />
); 

与仅仅渲染 <MyFeature> 时相比,这里现在发生的事情要多得多。让我们分析一下为什么会这样。这是你传递 addArticlearticleList 渲染属性的地方。这些属性值是接受来自 MyComponent 的参数值的函数。例如,onClickRemove() 函数来自 MyFeature,并用于改变该组件的状态。你可以使用 渲染属性 函数将此传递给将要渲染的组件,以及任何其他值。这些函数的返回值是最终要渲染的内容。

在本节中,你了解到通过传递渲染属性值:渲染 JSX 标记的函数,你可以避免在可能想要共享功能的地方硬编码依赖项。向组件传递不同的属性值通常比更改给定模块使用的依赖项更容易。

渲染组件树

让我们花点时间回顾一下本章到目前为止所取得的成果。曾经是单体的功能组件最终几乎完全专注于状态数据。它处理初始状态,处理转换状态,并且如果有的话,它会处理获取状态的网络请求。这是一个典型的 React 应用程序中的容器组件,也是数据的起点。

你为了更好地组合功能而实现的新的组件是这些数据的接收者。这些组件与它们的容器之间的区别在于,它们只关心在渲染时传入它们的属性。换句话说,它们只关心特定时间点的数据快照。从这里,这些组件可能会将属性数据作为属性传递给它们自己的子组件。组合 React 组件的通用模式如下:

图片

图 5.3:从较小组件组合较大 React 组件的模式

容器组件通常包含一个直接子组件。在这个图中,你可以看到容器包含一个项目详情组件或一个列表组件。当然,这两个类别中会有所变化,因为每个应用程序都是不同的。这种通用模式有三个组件组合级别。数据从容器单向流向底层的实用组件

一旦添加超过三层,应用程序架构就难以理解。可能会有需要添加四层 React 组件的异常情况,但作为一个经验法则,你应该避免这样做。

功能组件和实用组件

在本章中,我们讨论的单体组件示例中,你从一个完全专注于一个功能的单一组件开始。这意味着该组件在应用程序的其他地方几乎没有实用性。

原因在于顶层组件处理应用程序状态。有状态组件在其它任何上下文中都难以使用。随着你对单体功能组件进行重构,你创建了新的组件,这些组件进一步远离了数据。一般规则是,你的组件离有状态数据越远,它们的实用性就越大,因为它们的属性值可以从应用程序的任何地方传入。

概述

本章是关于避免单体组件设计。然而,单体通常是在任何 React 组件设计中必要的起点。

你首先学习了不同 HTML 元素具有不同程度的实用性。接下来,你了解了单体 React 组件的问题,并了解了单体组件的实现。

然后,你花费了几个章节学习如何将单体组件重构为更可持续的设计。从这个练习中,你了解到容器组件只需考虑处理状态,而较小的组件因为它们的属性值可以从任何地方传递,所以具有更多的实用性。你还了解到,你可以使用渲染属性来更好地控制组件依赖和替换。

在下一章中,你将学习关于组件属性验证和类型检查的内容。

第六章:使用 TypeScript 进行类型检查和验证

在本章中,我们将探讨在 React 组件中属性验证的重要性,以创建健壮、无错误的程序。我们将介绍 TypeScript,这是一种在 JavaScript 中进行静态类型检查的强大工具。

我们将指导您在项目中设置 TypeScript 并介绍其基本和高级概念。我们还将提供如何使用 TypeScript 在 React 组件中进行类型检查的示例。

到本章结束时,您将在属性验证和类型检查方面打下坚实的基础,并准备好使用 TypeScript 创建更可预测、更可靠的组件。

本章将涵盖以下主题:

  • 了解预期内容

  • TypeScript 简介

  • 在 React 中使用 TypeScript

技术要求

本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/React-and-React-Native-5E/tree/main/Chapter05

了解预期内容

在任何应用程序中,可预测性都是关键。一个可预测的应用程序会以预期的行为运行,减少错误,提高用户体验,并简化维护。当我们谈论 React 中的可预测性时,我们通常指的是组件根据其接收的属性如何行为。Props 是属性的简称,作为 React 组件的输入,决定了其行为和渲染。这就是 属性验证 概念发挥作用的地方。

属性验证的重要性

属性验证是一种确保组件接收正确类型数据的方法。它就像组件之间的合同。当组件指定它期望接收的属性类型时,它承诺如果接收了这些类型的属性,它将以某种方式行为。

属性验证对于以下几个原因至关重要:

  • 它有助于在开发过程中早期捕获错误:如果组件接收到了一个意外的属性类型,它可能不会按预期行为,导致难以追踪的错误。通过验证属性,我们可以在它们造成问题之前捕获这些错误。

  • 属性验证提高了代码可读性:通过查看组件的属性类型,您可以快速了解组件期望接收哪些数据。这使得在整个应用程序中使用和重用组件变得更加容易。

  • 属性验证使组件更具可预测性:当组件明确指定它期望接收的属性类型时,更容易理解组件将如何根据其属性行为。

没有属性验证的潜在问题

如果没有足够的属性验证,组件可能会变得不可预测,并容易产生错误。让我们看看一个组件:

const MyList = ({ list }) => (
  <ul>
    {list.map((user) => (
      <li key={user.name}>
        {user.name} ({user.email})
      </li>
    ))}
  </ul>
); 

在这个例子中,一个组件期望接收一个list prop,它应该是一个包含名称和电子邮件属性的数组对象。如果这个组件接收到的 list prop 是一个字符串、一个数字,甚至是一个数组,但没有对象,它可能会尝试访问user.nameuser.email,这会导致错误。

这种类型的错误可能很难调试,尤其是在具有许多组件的大型应用程序中。也可能很难理解我们应该向组件提供什么,而不必阅读这个组件的每一行代码。错误也可能导致应用程序崩溃或出现意外的行为。但如果我们能向我们的组件添加 props 验证,这可以帮助我们提前捕获这些错误并确保组件按预期行为?让我们来探索一下。

props 验证的选项

你可以使用几种工具在 React 和 React Native 中进行 props 验证。其中之一是PropTypes,这是一个库,允许你指定组件应接收的属性类型。另一个选项是 TypeScript,它是 JavaScript 的一个静态类型超集,提供了强大的类型检查工具。

现在,我想向您展示带有PropTypesMyList组件的示例。请看这个组件:

import PropTypes from 'prop-types';
const MyList = ({ list }) => (
  <ul>
    {list.map((user) => (
      <li key={user.name}>
        {user.name} ({user.email})
      </li>
    ))}
  </ul>
);
MyList.propTypes = {
  list: PropTypes.arrayOf(
    PropTypes.shape({
      name: PropTypes.string.isRequired,
      email: PropTypes.string.isRequired,
    })
  ).isRequired,
}; 

在这个例子中,我们使用PropTypes来指定list prop 应该是一个对象数组,并且每个对象都应该有一个nameemail属性,这两个属性都应该是字符串。

接下来,让我们看看 TypeScript 的示例:

type User = {
  name: string;
  email: string;
};
type MyListProps = {
  list: User[];
};
const MyList = ({ list }: MyListProps) => (
  <ul>
    {list.map((user) => (
      <li key={user.name}>
        {user.name} ({user.email})
      </li>
    ))}
  </ul>
); 

在这个 TypeScript 示例中,我们定义了一个User类型和一个MyListProps类型。User类型是一个具有nameemail属性的对象,这两个属性都是字符串。MyListProps类型是一个具有list属性的对象,该属性是一个User对象的数组。

虽然 PropTypes 和 TypeScript 都提供了用于 props 验证的有价值工具,但我们将在这本书的剩余部分专注于 TypeScript。TypeScript 提供了一种更全面、更强大的类型检查方法,并且在 React 和 React Native 社区中越来越受欢迎。

在接下来的章节中,所有示例都将使用 TypeScript。到这本书结束时,你将有一个扎实的 TypeScript 理解,并知道如何在你的 React 和 React Native 项目中使用它。所以,让我们深入探索 TypeScript 的世界吧!

TypeScript 简介

在我们开始学习类型检查和验证的旅程时,让我们暂时从 React 和 React Native 中抽身,将注意力转向 TypeScript。你可能想知道,“TypeScript 究竟是什么?”

TypeScript 是由微软开发和维护的 JavaScript 的静态类型超集。这意味着它为 JavaScript 添加了额外的功能,其中最显著的是静态类型。虽然 JavaScript 是动态类型的,但 TypeScript 引入了一个类型系统,允许你明确地定义变量、函数参数和函数返回值可以具有的数据类型。

但不用担心,TypeScript 与 JavaScript 完全兼容。事实上,任何有效的 JavaScript 代码也是有效的 TypeScript 代码。TypeScript 使用一个转译器(一种编译器类型)将 TypeScript 代码转换为浏览器无法直接理解的 JavaScript 代码,这样就可以在任何 JavaScript 可以运行的环境中运行。

考虑以下 JavaScript 函数:

function greet(name) {
  return "Hello, " + name;
}
console.log(greet("Mike")); // "Hello, Mike"
console.log(greet(32)); // "Hello, 32" 

当你传递一个字符串作为参数时,这个函数按预期工作。但是,如果你传递一个数字,它不会抛出错误,尽管问候一个数字在逻辑上并不合理。

现在,让我们看看我们如何用 TypeScript 编写这个函数:

function greet(name: string) {
  return "Hello, " + name;
}
console.log(greet("Mike")); // "Hello, Mike"
console.log(greet(32)); // Error: Argument of type 'number' is not assignable to parameter of type 'string'. 

在 TypeScript 版本中,我们为name参数添加了类型注解。这告诉 TypeScriptname始终应该是字符串类型。如果我们尝试用数字调用greet,TypeScript 会给出错误。这有助于我们在运行代码之前就捕获到错误。

这是一个简单的例子,但它说明了 TypeScript 的一个关键好处:它可以帮助我们在错误导致代码中的 bug 之前尽早捕获错误。这就像有一个有用的副驾驶,在问题变得严重之前指出潜在的问题。

为什么使用 TypeScript?

现在我们已经介绍了 TypeScript 是什么,让我们深入探讨为什么你可能想在项目中学习和使用它:

  • 尽早捕获错误:我们之前已经讨论过这一点,但将其放在列表的第一位仍然很有价值。TypeScript 最大的优点之一就是能够在编译时捕获错误,甚至在代码运行之前。这有助于防止许多在常规 JavaScript 中可能直到运行时才被捕获的常见错误。

  • 提高代码可读性:TypeScript 的类型注解使得函数期望的参数类型或函数返回的类型值一目了然。这使得代码更容易阅读和理解,尤其是对于可能正在同一代码库上工作的其他开发者来说。

  • 更容易重构:TypeScript 的静态类型还使得代码重构变得更加容易。如果你更改变量的类型或函数的签名,TypeScript 可以帮助你找到代码中需要做出相应更改的所有位置。

  • 社区和工具支持:TypeScript 在 JavaScript 社区中获得了显著的流行度,并被微软、谷歌和 Airbnb 等许多大型公司使用。这意味着有一个庞大的开发者社区可以提供支持,并提供丰富的学习 TypeScript 的资源。此外,许多代码编辑器对 TypeScript 提供了出色的支持,提供如 自动完成类型推断错误突出显示等功能。

  • 与现代框架和库的集成:TypeScript 与现代 JavaScript 框架如 React 和 React Native 集成良好,这些框架内置了 TypeScript 定义,使得构建强类型应用变得更加容易。此外,大多数流行的 JavaScript 库都提供了 TypeScript 定义。这些定义通常由社区贡献,提供了关于库函数和对象类型信息,使得在 TypeScript 项目中使用这些库更加容易和安全。

    这种在 JavaScript 生态系统中对 TypeScript 的广泛应用确保了你可以几乎在代码库的任何地方利用 TypeScript 的益处。

  • 增加就业市场需求:TypeScript 的流行不仅限于开发实践:它在就业市场上也越来越受欢迎。许多公司,从小型初创公司到大型企业,都在其项目中采用 TypeScript,因此对熟练掌握 TypeScript 的开发者的需求不断增长。这对于涉及 React 和 React Native 的职位尤其如此,在这些职位中,TypeScript 通常用于其在大规模代码库扩展和维护方面的优势。通过学习 TypeScript,你不仅为你的项目获得了一项宝贵的技能,而且作为开发者,也使自己在市场上更具竞争力。

总结来说,TypeScript 提供了一系列可以帮助你编写更健壮、可维护代码的益处。它是任何 JavaScript 开发者工具箱中的宝贵工具,其在就业市场上的日益流行使其成为你职业发展的值得投资。

但理解 TypeScript 的好处只是第一步。要真正发挥其威力,你需要知道如何在项目中使用它。在下一节中,我们将指导你通过在 React 项目中设置 TypeScript 的过程。我们将从安装 TypeScript 到配置项目使用 TypeScript 的所有内容进行讲解。那么,让我们深入探索 TypeScript 的实际应用吧!

在项目中设置 TypeScript

在第一章中,我们介绍了使用 Vite 创建新 React 项目的流程。现在,让我们看看如何创建一个 TypeScript 项目。

Vite 为创建新的 React 和 TypeScript 项目提供了一个模板。你可以使用以下命令创建一个新项目:

npm create vite@latest my-react-app -- --template react-ts 

此命令使用 react-ts 模板创建一个新的 Vite 项目,该模板包含 TypeScript。基于此模板的项目将在您的项目根目录中包含 tsconfig.json 文件。此文件用于为您的项目配置 TypeScript。

下面是 tsconfig.json 文件可能的样子:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "jsx": "react-jsx",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true
  }
} 

这些设置告诉 TypeScript 将您的代码编译为最新版本的 JavaScript ("target": "esnext"), 使用最新的模块系统 ("module": "esnext"), 并使用 React 17 中引入的新 JSX 转换 ("jsx": "react-jsx"). "strict": true 选项启用了一组广泛的类型检查行为,以捕获更多问题。

在设置好 TypeScript 之后,让我们编写一些代码。然而,TypeScript 使用与 JavaScript 不同的文件扩展名:没有 JSX 的文件使用 *.ts 扩展名,而带有 JSX 的文件使用 *.tsx 扩展名。因此,让我们使用 TypeScript 创建我们的第一个 React 组件:

type AppProps = {
  message: string;
};
function App({ message }: AppProps) {
  return <div>{message}</div>;
} 

在这个例子中,我们正在为 App 组件的 props 定义一个 AppProps 类型。这告诉 TypeScript,message prop 应该是一个字符串。

现在,让我们看看 main.tsx 现在的样子:

图片 2

图 6.1:main.tsx 文件中的 App 组件及其 TypeScript 错误

这就是 TypeScript 如何检查和验证组件中 props 的使用。在这里,我们应该传递 message prop:

图片 3

图 6.2:main.tsx 文件中没有错误的 App 组件

最后,您可以使用以下命令运行您的项目:

npm run dev 

此命令启动 Vite 开发服务器。如果您的代码中存在任何 类型错误,TypeScript 也会在控制台中显示它们。

TypeScript 中的基本类型

TypeScript 的一个关键特性是其丰富的类型系统。TypeScript 引入了几种基本类型,您可以使用它们来描述您数据的结构。要指定变量的类型,您在变量名后使用冒号,然后跟类型。

让我们探索这些基本类型:

  • 布尔值: 最基本的类型是简单的 true/false 值,JavaScript 和 TypeScript 称其为 Boolean:

    let isDone: boolean = false; 
    
  • 数字: 与 JavaScript 一样,TypeScript 中的所有数字都是浮点值。这些浮点数获得 number 类型:

    let age: number = 32; 
    
  • 字符串: 在 JavaScript 中创建用于网页和服务器程序的基本部分之一是处理 文本数据。与其他语言一样,我们使用 string 类型来引用这些文本数据类型:

    let color: string = "blue"; 
    
  • 数组: TypeScript,就像 JavaScript 一样,允许您使用值数组。数组类型可以用两种方式之一来编写。在第一种方式中,您使用元素的类型,然后跟上一个 [] 来表示该元素类型的数组:

    let list: number[] = [1, 2, 3]; 
    

    第二种方式使用泛型数组类型,Array<elemType>

    let list: Array<number> = [1, 2, 3]; 
    
  • 元组: 元组类型允许您表达一个数组,其中已知固定数量元素的类型,但不需要它们相同。例如,您可能希望将一个值表示为一个 string 和一个 number 的对:

    let x: [string, number];
    x = ["hello", 10]; // OK 
    
  • 枚举:JavaScript 标准数据类型集的一个有用补充是 enum。类似于 C# 等语言,enum 是为数值集合提供更友好名称的一种方式:

    enum Color {
      Red,
      Green,
      Blue,
    }
    let c: Color = Color.Green; 
    
  • Any:当我们编写应用程序时,我们可能需要描述我们不知道的变量的类型。这些值可能来自动态内容,例如用户或第三方库。在这些情况下,我们希望退出类型检查并让值通过编译时检查。为此,我们用 any 类型标记这些值:

    let notSure: any = 4;
    notSure = "maybe a string instead";
    notSure = false; // okay, definitely a Boolean 
    
  • Unknownunknown 类型是 any 的类型安全对应物。任何东西都可以赋值给 unknown,但 unknown 只能赋值给自己和 any(在没有类型断言或基于控制流的缩窄的情况下)。同样,在没有首先断言或缩窄到更具体的类型之前,不允许对 unknown 执行任何操作:

    let notSure: unknown = 4;
    notSure = "maybe a string instead";
    // OK, because of structural typing
    notSure = false; 
    let surelyNotAString: string = notSure; // Error, 'unknown' is not assignable to 'string' 
    

    在这个例子中,如果没有类型检查,我们不能将 notSure 赋值给 surelyNotAString,因为 notSureunknown 类型。这有助于防止错误,因为我们不能在不首先检查其类型的情况下意外地对 unknown 类型的变量执行操作。

    unknown 的一个常见用例是在 catch 子句中,其中 error 对象的类型是未知的:

    try {
      // some operation that might throw
    } catch (error: unknown) {
      if (error instanceof Error) {
        console.log(error.message);
      }
    } 
    

    在这个例子中,我们不知道 error 类型可能是什么,所以我们给它赋予 unknown 类型。这迫使我们在与它交互之前检查其类型。

  • Voidvoid 类似于 any 的对立面:完全没有类型。你可能会常见到它是没有返回值的函数的返回类型:

    function warnUser(): void {
      console.log("This is my warning message");
    } 
    
  • Null 和 undefined:在 TypeScript 中,undefinednull 实际上分别有自己的类型,分别命名为 undefinednull。与 void 类似,它们本身并不非常有用:

    let u: undefined = undefined;
    let n: null = null; 
    

    然而,undefined 在可选类型中起着至关重要的作用。在 TypeScript 中,你可以在类型名称后添加 ? 来使一个类型可选。这意味着值可以是指定的类型或 undefined。例如:

    function greet(name?: string) {
      return 'Hello ${name}';
    }
    greet("Mike");
    greet(undefined); // OK
    greet(); // Also OK 
    
  • 永不:在 TypeScript 中,never 类型代表一种永远不会发生值的类型。它在函数永远不会返回值或达到其执行路径的末尾时使用。例如,抛出错误的函数或具有无限循环的函数可以用 never 类型进行注解:

    function throwError(errorMsg: string): never {
        throw new Error(errorMsg);
    }
    function infiniteLoop(): never {
        while (true) {
        }
    } 
    

理解这些基本类型是在 TypeScript 中工作的关键第一步。当你开始在项目中使用 TypeScript 时,你会发现这些类型是编写健壮、可维护代码的强大工具。

在下一节中,我们将更深入地探讨 TypeScript 的类型系统,并探索接口和类型别名,它们提供了一种定义复杂类型的方法。

接口和类型别名

虽然基本类型对于简单数据类型很有用,但在处理更复杂的数据结构时,我们需要更强大的工具。这就是接口和类型别名发挥作用的地方。它们允许我们定义复杂类型并给它们命名。

接口

在 TypeScript 中,接口是一种定义复杂类型合同的方式。它描述了一个对象应该具有的形状。以下是一个示例:

interface User {
  name: string;
  email: string;
} 

在这个示例中,我们定义了一个具有两个属性nameemailUser接口,这两个属性都是字符串类型。我们可以使用这个接口来进行对象类型检查:

const user: User = {
  name: "Alice",
  email: "alice@example.com",
}; 

如果我们尝试将一个不符合User接口的对象赋值给用户变量,TypeScript 将会给我们一个错误。

类型别名

类型别名与接口非常相似,但也可以用于其他类型,而不仅仅是对象。以下是一个type别名的示例:

type Point = {
  x: number;
  y: number;
};
type ID = number | string; 

在这个示例中,我们定义了一个Point类型,它代表二维空间中的一个点,以及一个可以是字符串或数字的ID。我们可以像使用接口一样使用这些type别名:

const point: Point = {
  x: 10,
  y: 20,
};
const id: ID = 100; 

接口与类型别名

那么,何时应该使用接口,何时应该使用类型别名?在许多情况下,两者可以互换,这主要是个人的偏好问题。

然而,也有一些不同之处。接口更具有可扩展性,因为它们可以被多次声明,并且会合并在一起。类型别名不能重新打开以添加新属性。另一方面,类型别名可以表示其他类型,如联合类型、交叉类型、元组以及其他在接口中目前不可用的类型。

通常,如果你正在定义对象的形状,无论是接口还是类型别名都可以。如果你正在定义一个可能不是对象的类型,你需要使用类型别名。

在本节中,我们迈出了 TypeScript 世界的第一步。我们学习了在Vite项目中设置 TypeScript、其基本类型以及如何使用接口和类型别名定义复杂类型。

现在,让我们探索如何将 TypeScript 与 React 组件、状态、事件处理程序一起使用。

在 React 中使用 TypeScript

好的,我们已经走到这一步了!我们已经学习了 TypeScript 的基础知识,并讨论了它的好处。现在,是时候卷起袖子,用一些实际的 TypeScript 在 React 中动手实践了。

在本节中,我们将探讨如何使用 TypeScript 来检查 React 应用程序的所有不同部分。我们将查看组件、props、状态、事件处理程序、上下文,甚至是 refs。不用担心:我会通过大量的示例来帮助你理解这些概念。

在 React 组件中检查 props

在一个 React 应用程序中,我们可以利用 TypeScript 的主要领域之一是在我们的组件中,特别是与 props 相关。让我们看看示例:

type GreetingProps = {
  name: string;
};
const Greeting = ({ name }: GreetingProps) => {
  return <h1>Hello, {name}!</h1>;
}; 

在这个例子中,我们定义了一个GreetingProps类型,它指定了Greeting应该接收的 props 的形状。然后我们使用这个类型来检查Greeting组件中的name prop。

这是一个只有一个 props 的简单例子,但同样的方法也可以用于具有更复杂 props 的组件。例如,如果一个组件接收一个对象或数组作为 props,我们可以定义一个类型来描述该对象或数组的形状。以下是一个例子:

type UserProps = {
  user: {
    name: string;
    email: string;
  };
};
const UserCard = ({ user }: UserProps) => {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}; 

在这个例子中,UserCard组件接收一个包含nameemail属性的对象类型的user属性。我们定义一个UserProps类型来描述这个对象的形状,并使用它来检查user属性的类型。

让我们考虑 React 中另一个常见的场景:可选 props。有时,一个组件的 props 不是总是必需的。在这些情况下,我们可以为 prop 提供一个默认值,并在我们的类型定义中将其标记为可选。以下是一个例子:

type ButtonProps = {
  children: React.ReactNode;
  disabled?: boolean;
};
const Button = ({ children, disabled = false }: ButtonProps) => {
  return <button disabled={disabled}>{children}</button>;
}; 

ButtonProps类型中,我们使用React.ReactNode作为children prop 的类型。这是 React 提供的一个特殊类型,可以接受任何可渲染的内容。这包括字符串、数字、JSX 元素、这些类型的数组,甚至是返回这些类型的函数。通过使用React.ReactNode,我们表示children prop 可以是 React 可以渲染的任何类型的内容。此外,我们还使用了可选的disabled prop。我们通过在ButtonProps类型中将disabled prop 名称后面添加一个?来表示disabled是可选的。我们还在组件函数参数中为disabled提供了默认值 false。

这样,我们可以在有或没有disabled prop 的情况下使用Button组件,TypeScript 仍然会正确地进行类型检查:

<Button>Click me!</Button> // OK
<Button disabled>Don't click me!</Button> // OK 

类型化状态

正如我们对 props 进行了类型检查一样,我们也可以使用 TypeScript 来检查组件中的状态。这确保了我们始终使用正确的状态值类型,为我们的代码提供了另一层安全保障。

让我们看看如何将 TypeScript 应用于函数组件中的状态的例子:

const Counter = () => {
  const [count, setCount] = React.useState<number>(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </div>
  );
}; 

在这个Counter组件中,我们使用React.useState<number>(0)来声明一个初始值为0的状态变量count。通过将<number>作为useState的类型参数,我们告诉 TypeScript count始终应该是数字类型。顺便说一句:我们可以省略传递<number>,因为 TypeScript 足够智能,可以根据初始值的类型推断出count应该是数字类型。

这也意味着setCount函数只会接受数字。如果我们尝试用非数字参数调用setCount,TypeScript 会给我们一个错误。

类型化事件处理器

另一个 TypeScript 在 React 应用程序中非常有用的领域是在事件处理器。通过类型检查我们的事件处理器,我们可以确保我们使用正确的事件类型,并访问事件对象上的正确属性。

让我们看看一个具有输入字段和类型化事件处理器的函数组件的例子:

const InputField = () => {
  const [value, setValue] = React.useState("");
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue(event.target.value);
  };
  return <input value={value} onChange={handleChange} />;
}; 

在这个InputField组件中,我们定义了一个handleChange函数,该函数将在输入字段值变化时被调用。我们使用React.ChangeEvent<HTMLInputElement>类型作为事件参数,以指定这个函数应该接收来自输入字段的更改事件。

这个类型包括我们从输入字段更改事件中期望的所有属性,例如event.target.value。如果我们尝试访问这个类型上不存在的属性,TypeScript 会给我们一个错误。

类型化上下文

当使用 TypeScript 与 React 时,我们还可以对上下文进行类型检查,以确保我们始终使用正确的值类型。让我们看看一个例子:

type ThemeContextType = {
  theme: string;
  setTheme: (theme: string) => void;
};
const ThemeContext = React.createContext<ThemeContextType | null>(null);
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setTheme] = React.useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};
const useTheme = () => {
  const context = React.useContext(ThemeContext);
  if (context === null) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}; 

在这个例子中,我们使用React.createContext创建了一个ThemeContext。我们向createContext提供一个ThemeContextType作为类型参数,以指定上下文值的形状。这个类型包括一个主题字符串和一个setTheme函数。

然后,我们创建一个ThemeProvider组件,它将主题和setTheme值提供给上下文。在useTheme钩子内部,我们使用React.useContext来消费ThemeContext。如果上下文是null,我们抛出一个错误。

这是一个常见的模式,以确保在提供者内部使用上下文。

通过这个例子,我想强调 TypeScript 的一个重要特性。在useTheme钩子中,我们不需要指定类型。它返回上下文值,TypeScript 知道它是ThemeContextType类型而不是null,这要归功于错误检查。这意味着当我们使用useTheme时,TypeScript 会自动提供正确的非空上下文类型。

输入引用

现在,让我们将注意力转向 React 中的另一个强大特性:refs。正如你从第三章理解 React 组件和 Hooks中已经知道的,refs 给我们提供了一个在组件内部直接访问DOM 节点React 元素的方法。但我们是怎样确保正确使用 refs 的呢?TypeScript 来帮忙。

考虑这个例子,我们将 TypeScript 应用到 refs 上:

const InputWithRef = () => {
  const inputRef = React.useRef<HTMLInputElement>(null);
  const focusInput = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
}; 

在这个InputField组件中,我们使用React.useRef创建了一个引用。我们向useRef提供一个HTMLInputElement作为类型参数,以指定引用的类型。HTMLInputElement是 TypeScript 内置 DOM 类型提供的一个类型,它代表 DOM 中的一个输入元素。这个类型对应于引用附加到的 DOM 元素的类型。

这意味着inputRef.current将是HTMLInputElement | null类型,TypeScript 会知道它有一个focus方法。

摘要

在这一章中,我们深入探讨了 React 中的类型检查和验证的世界。我们从属性验证的重要性开始,然后介绍了 TypeScript 及其在健壮类型检查方面的好处。

我们随后将 TypeScript 应用于 React,展示了它在检查 React 组件各个方面(从 props 和 state 到事件处理器、上下文和 refs)中的使用。所有这些功能都允许你创建不仅更可靠而且更容易维护的应用程序,能够早期发现错误,显著提高你的代码质量和作为开发者的效率。

随着我们进入下一章,使用路由处理导航,我们将把我们的重点转向 React 应用程序中的导航。我们将学习如何设置和使用路由在应用程序的不同部分之间进行导航。