React 挂钩学习指南第二版(一)
原文:
zh.annas-archive.org/md5/e3f80e0bbd9c0adfcf30deda2265e9fb译者:飞龙
前言
你好——我是丹尼尔,一个企业家、软件开发顾问和全栈开发者,专注于 React 生态系统中的技术。
在我作为软件开发顾问和为企业及公共部门工作的开发者期间,我观察到的一个共同挑战是:由于缺乏深入理解,开发者往往难以掌握高级 React 概念。Hooks 尤其是一个很大的困惑来源。通常,很难知道最佳实践是什么,以及如何最好地使用 React 和 React Hooks 来构建和设计应用程序。
在这本书中,我想教你构建现代且可维护的前端所需的所有知识,我会从零开始教授 Hooks,以确保你理解它们的限制和优势所在。我会涵盖我在职业生涯中经常遇到的各种常见用例,例如管理应用程序状态、何时以及如何使用 React Contexts、数据获取、表单处理和路由。我还会教你何时以及如何构建自己的 Hooks,以保持应用程序的可维护性并高效地在多个组件之间重用逻辑。在我看来,自定义 Hooks 在项目中往往被低估,但它们可以带来巨大的价值,尤其是在大型项目中。
所有这些概念都将通过实际示例进行教学,以便你能够立即看到它们的应用,并在自己的项目中开始使用它们。我真诚地希望你喜欢阅读这本书。如果你有任何问题或反馈,请随时与我联系!
这本书面向的对象
这本书是为已经知道如何使用 React 的开发者准备的,他们想深入了解 React Hooks 以及与之相关的现代技术,如表单操作、Context 和 Suspense。即使你已经了解 Hooks,这本书也会教你它们是如何内部工作的,以便你能更深入地理解它们。此外,你还将学习一些技巧和窍门,以及如何有效地开发 React 应用程序的最佳实践。
这本书涵盖的内容
第一章,介绍 React 和 React Hooks,涵盖了 React 和 React Hooks 的基本原理,以及如何使用 React 设置一个现代项目。
第二章,使用 State Hook,通过重新实现和使用 State Hook 来深入解释 Hooks 的工作原理,在学习过程中了解 Hooks 的限制。
第三章,使用 React Hooks 编写您的第一个应用程序,将我们从前两章学到的知识付诸实践,通过创建一个使用 React Hooks 的博客应用程序来应用这些知识。
第四章,使用 Reducer 和 Effect Hooks,介绍了这两个基本 Hooks,重点关注在博客应用程序中实现它们时何时以及如何使用它们。
第五章,实现 React 上下文,解释了 React Context 及其在应用程序中的应用,以及与 Hooks 结合使用。
第六章,使用 Hooks 和 React Suspense 进行数据获取,涵盖了使用 Effect 和 State Hooks 从服务器请求资源。然后,我们学习如何使用 TanStack Query、React Suspense 和错误边界更有效地请求资源。
第七章,使用 Hooks 处理表单,深入探讨了使用 React 处理表单,特别是关注新的范式,如表单操作、过渡和乐观更新。
第八章,使用 Hooks 进行路由,介绍了 React Router,并展示了如何使用 Hooks 从路由中获取参数,以及触发动态路由变化。
第九章,React 提供的高级 Hooks,概述了 React 提供的所有内置 Hooks,重点关注书中尚未涵盖的所有高级 Hooks。
第十章,使用社区 Hooks,概述了 React 社区提供的各种有用 Hooks,以及如何找到更多 Hooks 的信息。
第十一章,Hooks 规则,教你开始构建自己的 Hooks 所需了解的规则。
第十二章,构建自己的 Hooks,介绍了如何通过将现有逻辑提取到 Hook 中来构建自己的自定义 Hooks。通过了解何时以及如何创建自定义 Hooks,你将能够以可扩展的方式重构和维护你的应用程序。
第十三章,从 React 类组件迁移,提供了一篇关于如何有效地将现有应用程序从 React 类组件迁移到 React Hooks 的指南。
为了充分利用这本书
应该已经安装了相当新版本的 Node.js。Node 包管理器(npm)也需要安装(它应该随 Node.js 一起提供)。有关如何安装 Node.js 的更多信息,请访问他们的官方网站:nodejs.org/。
我们将在本书的指南中使用Visual Studio Code(VS Code),但任何其他编辑器都应该以类似的方式工作。有关如何安装 VS Code 的更多信息,请参阅他们的官方网站:code.visualstudio.com。
在这本书中,我们使用以下版本:
-
Node.js v22.14.0
-
npm v10.9.2
-
VS Code v1.97.2
上列版本是本书中使用的版本。虽然安装较新版本不应有问题,但请注意,某些步骤在较新版本上可能有所不同。如果您在使用本书提供的代码和步骤时遇到问题,请尝试使用提到的版本。
如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。
下载示例代码文件
本书代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Learn-React-Hooks-Second-Edition/。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/gbp/9781836209171。
使用的约定
本书使用了多种文本约定。
CodeInText: 表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“然后,我们定义componentDidMount生命周期方法,从 API 中获取数据。”
代码块设置如下:
fetchData() {
fetch(`http://my.api/${this.props.name}`)
.then(...)
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
componentDidMount() {
**this****.****fetchData****()**
}
componentDidUpdate(prevProps) {
if (this.props.name !== prevProps.name) {
**this****.****fetchData****()**
}
}
任何命令行输入或输出都应如下编写:
$ npm create vite@6.2.0 .
粗体: 表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会像这样显示。例如:“应该打开一个侧边栏,您可以在顶部看到市场中的搜索扩展。在此处输入扩展名,然后点击安装来安装它。”
警告或重要提示看起来像这样。
提示和技巧看起来像这样。
联系我们
欢迎读者反馈。
一般反馈: 请通过电子邮件发送至feedback@packtpub.com,并在邮件主题中提及本书的标题。如果您对本书的任何方面有疑问,请通过电子邮件发送至questions@packtpub.com。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,点击提交勘误,并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com/。
分享您的想法
一旦您阅读了《Learn React Hooks》第二版,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在路上阅读,但又无法随身携带您的印刷书籍吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在,您每购买一本 Packt 书籍,都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。
按照以下简单步骤获取优惠:
- 扫描下面的二维码或访问以下链接:
packt.link/free-ebook/9781836209171
-
提交您的购买证明。
-
就这些了!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件。
第一部分
钩子简介
在本部分中,你将学习如何设置一个现代的 React 项目,以及了解 React 和 React 钩子的基础知识。你还将学习为什么以及如何使用钩子。在本部分的最后,你将构建一个博客应用程序,这将为本书中所有后续章节奠定基础。在这个过程中,你还将了解最新的 JavaScript 和 React 功能。
本部分包含以下章节:
-
第一章, 介绍 React 和 React 钩子
-
第二章, 使用 State 钩子
-
第三章, 使用 React 钩子编写你的第一个应用程序
第一章:介绍 React 和 React Hooks
React 是一个用于构建高效和可扩展 Web 应用的 JavaScript 库。React 由 Meta 开发,并被用于许多大型 Web 应用程序中,如 Facebook、Instagram、Netflix、Shopify、Airbnb、Cloudflare 和 BBC。
在本书中,我们将学习如何使用 React 构建复杂且高效的用户界面,同时保持代码简单和可扩展。通过 React Hooks 的范式,我们可以极大地简化处理 Web 应用程序中的状态和副作用,确保应用程序未来有增长和扩展的潜力。我们还将了解 React Context、React Suspense 和 表单操作,以及它们如何与 Hooks 一起使用。最后,我们将学习如何构建自己的 Hooks 以及如何将现有应用程序从 React 类组件迁移到基于 React Hooks 的架构。
在本章中,我们将学习 React 和 React Hooks 的基本原理。我们将从了解 React 和 React Hooks 是什么以及为什么我们应该使用它们开始。然后,我们将继续学习 Hooks 的内部工作原理。最后,您将了解 React 提供的 Hooks 以及社区提供的几个 Hooks,例如数据获取和路由 Hooks。通过学习 React 和 React Hooks 的基础知识,我们将更好地理解本书中将要介绍的概念。
在本章中,我们将涵盖以下主要主题:
-
React 原理
-
使用 React Hooks 的动机
-
设置开发环境
-
开始使用 React Hooks
技术要求
应该已经安装了相当新版本的 Node.js。Node 包管理器 (npm) 也需要安装(它应该随 Node.js 一起安装)。有关如何安装 Node.js 的更多信息,请查看他们的官方网站:nodejs.org/.
在本书的指南中,我们将使用 Visual Studio Code (VS Code),但所有内容在任何其他编辑器中都应该类似。有关如何安装 VS Code 的更多信息,请参阅他们的官方网站:code.visualstudio.com.
在本书中,我们使用以下版本:
-
Node.js v22.14.0
-
npmv10.9.2 -
Visual Studio Code v1.97.2
上列出的版本是本书中使用的版本。虽然安装较新版本不应有问题,但请注意,某些步骤在较新版本上可能会有所不同。如果您在使用本书中提供的代码和步骤时遇到问题,请尝试使用提到的版本。
您可以在 GitHub 上找到本章的代码:github.com/PacktPublishing/Learn-React-Hooks-Second-Edition/tree/main/Chapter01.
强烈建议您自己编写代码。不要简单地运行书中提供的代码示例。自己编写代码对于正确学习和理解代码非常重要。然而,如果您遇到任何问题,您始终可以参考代码示例。
React 原则
在我们开始学习如何设置 React 项目之前,让我们回顾一下 React 的三个基本原理。这些原理使我们能够轻松编写可扩展的 Web 应用程序。
React 基于三个基本原理:
-
声明式:我们不是告诉 React 如何做事,而是告诉它我们想要它做什么。例如,如果我们更改数据,我们不需要告诉 React 哪些组件需要重新渲染。这会很复杂且容易出错。相反,我们只需告诉 React 数据已更改,所有使用此数据的相关组件都将被高效更新并为我们重新渲染。React 会处理细节,这样我们就可以专注于手头的任务,轻松开发我们的 Web 应用程序。
-
组件化:React 封装了管理自身状态和视图的组件,然后允许我们将它们组合起来以创建复杂用户界面。
-
一次学习,到处编写:React 不对您的技术栈做出假设,并试图确保您可以在不尽可能重写现有代码的情况下开发应用程序。
React 的三个基本原理使编写代码、封装组件以及在多个平台上共享代码变得容易。React 总是试图尽可能多地利用现有的 JavaScript 功能,而不是重新发明轮子。因此,我们将学习适用于许多更多情况的软件设计模式,而不仅仅是设计用户界面。
我们刚刚了解到 React 是基于组件的。在 React 中,有两种类型的组件:
-
函数组件:接受 props 作为参数的 JavaScript 函数,并返回用户界面(通常通过 JSX,它是 JavaScript 语法的一个扩展,允许我们在 JavaScript 代码中直接编写类似 HTML 的标记)
-
类组件:提供
render方法的 JavaScript 类,该方法返回用户界面(通常通过 JSX)
虽然函数组件更容易定义和理解,但在过去,处理状态、上下文以及许多其他 React 高级功能需要类组件。然而,随着 React Hooks 的出现,我们可以使用 React 的大多数高级功能,而无需类组件!
在编写本文时,React 拥有一些特性,这些特性目前还不能通过函数组件和 Hooks 实现。例如,定义错误边界仍然需要类组件以及 componentDidCatch 和 getDerivedStateFromError 生命周期方法。
使用 React Hooks 的动机
React 总是努力使开发者体验尽可能顺畅,同时确保性能足够好,让开发者无需过多担心如何优化性能。然而,在多年的 React 使用过程中,已经识别出了一些问题。
在以下子节中的代码片段仅旨在让您了解为什么需要 Hooks,通过给出开发者以前如何处理 React 中某些问题的示例。如果您不熟悉这些旧方法,请不要担心,了解旧方法并不是继续学习所必需的。在接下来的章节中,我们将学习如何使用 React Hooks 以更好的方式处理这些问题。
现在,让我们在以下子节中查看这些问题。
令人困惑的类
在过去,我们必须使用具有特殊函数的生命周期方法(如 componentDidUpdate)和特殊的状态处理方法(如 this.setState)来处理状态变化。React 类,尤其是 this 上下文,对于开发者和机器来说都很难阅读和理解。
this 是 JavaScript 中一个特殊的保留字,它始终指向它所属的对象:
-
在方法中,
this指的是类对象(类的实例)。 -
在事件处理程序中,
this指的是接收事件的元素。 -
在函数或独立存在时,
this指的是全局对象。例如,在浏览器中,全局对象是Window对象。 -
在严格模式下,函数中的
this是undefined。 -
此外,
call()和apply()等方法可以改变this指向的对象,因此它可以指向任何对象。
对于开发者来说,类很难,因为 this 总是指向不同的事物,所以有时(例如在事件处理程序中)我们需要手动将其重新绑定到类对象上。对于机器来说,类也很难,因为它们不知道类中哪些方法会被调用以及 this 将如何被修改,这使得优化性能和删除未使用的代码变得困难。
此外,类有时要求我们在多个地方同时编写代码。例如,如果我们想在组件渲染时获取数据或组件的 props 发生变化时,我们需要使用两种方法来完成:一次在 componentDidMount 中,一次在 componentDidUpdate 中。
为了举例说明,让我们定义一个从 API 获取数据的类组件:
-
首先,我们通过扩展
React.Component类来定义一个类组件:class ExampleComponent extends React.Component { -
然后,我们定义
componentDidMount生命周期方法,在那里我们从 API 拉取数据:componentDidMount() { fetch(`http://my.api/${this.props.name}`) .then(…) } -
然而,我们还需要定义
componentDidUpdate生命周期方法,以防nameprop 发生变化。此外,我们还需要在这里添加一个手动检查,以确保我们仅在nameprop 发生变化时重新获取数据,而不是在其他 props 发生变化时:componentDidUpdate(prevProps) { if (this.props.name !== prevProps.name) { fetch(`http://my.api/${this.props.name}`) .then(...) } } } -
为了使代码不那么重复,我们可以通过创建一个名为
fetchData的单独方法来重构我们的代码,并如下获取数据:fetchData() { fetch(`http://my.api/${this.props.name}`) .then(...) } -
然后,我们可以在
componentDidMount和componentDidUpdate中调用该方法:componentDidMount() { **this****.****fetchData****()** } componentDidUpdate(prevProps) { if (this.props.name !== prevProps.name) { **this****.****fetchData****()** } }
然而,即使如此,我们仍然需要在两个地方调用方法。每当我们需要更新传递给方法的参数时,我们都需要在两个地方更新它们,这使得这种模式非常容易出错,并且可能导致未来的错误。
包装地狱
假设我们已实现了一个添加认证到我们组件之一的authenticateUser高阶组件函数,以及一个名为AuthenticationContext的上下文,通过渲染属性提供有关认证用户的信息。然后,我们会如下使用此上下文:
-
我们首先导入
authenticateUser函数,用上下文包装我们的组件,并导入AuthenticationContext组件以便能够访问上下文:import authenticateUser, { AuthenticationContext } from './auth' -
然后,我们定义一个
App组件,在其中我们使用AuthenticationContext.Consumer组件和user渲染属性:const App = () => ( <AuthenticationContext.Consumer> {user =>
渲染属性是将属性传递到组件子组件的一种方式。正如我们所见,渲染属性允许我们将user传递给AuthenticationContext.Consumer组件的子组件。
-
现在,我们根据用户是否登录显示不同的文本:
user ? `${user} logged in` : 'not logged in' }
在这里,我们使用了两个 JavaScript 概念:
-
三元运算符是
if条件的内联版本。它看起来如下:ifThisIsTrue ? returnThis : otherwiseReturnThis。 -
模板字符串可以用来在字符串中插入变量。它使用反引号(
`)而不是普通单引号(')来定义。变量可以通过${variableName}语法插入。我们也可以在${}括号内使用任何 JavaScript 表达式——例如,${someValue + 1}。
-
最后,通过使用高阶组件模式,我们在将
authenticateUser上下文包装到组件后导出组件:</AuthenticationContext.Consumer> ) export default authenticateUser(App)
高阶组件是包装组件并为其添加功能的函数。在 Hooks 出现之前,它们被用来封装和重用状态管理逻辑。
在这个例子中,我们使用了authenticateUser高阶组件函数来为我们现有的组件添加认证逻辑。然后,我们使用AuthenticationContext.Consumer通过其渲染属性将user属性注入到我们的组件中。
如你所想,使用许多高阶组件会导致一个具有许多子树的庞大树,这是一种称为包装地狱的反模式。例如,当我们想要使用三个上下文时,包装地狱看起来如下:
<AuthenticationContext.Consumer>
{user => (
<LanguageContext.Consumer>
{language => (
<StatusContext.Consumer>
{status => (
...
)}
</StatusContext.Consumer>
)}
</LanguageContext.Consumer>
)}
</AuthenticationContext.Consumer>
这并不容易阅读或编写,如果以后需要更改某些内容,也容易出错。包装地狱使得调试变得困难,因为我们需要查看一个具有许多仅作为包装器的组件的大型组件树。
现在我们已经了解了 React 的一些常见问题,让我们学习 Hook 模式,以便更好地处理这些问题!
Hooks 来拯救!
React Hooks 基于与 React 相同的基本原则。它们通过使用现有的 JavaScript 特性来封装状态管理。因此,我们不再需要学习和理解许多专门的 React 特性;我们只需利用我们现有的 JavaScript 知识来使用 Hooks。
使用 Hooks,我们可以为之前提到的问题提出更好的解决方案。Hooks 简单来说就是可以在函数组件中调用的函数。我们也不再需要使用 render props 来处理上下文,因为我们可以直接使用 Context Hook 来获取所需的数据。此外,Hooks 允许我们在组件之间重用有状态的逻辑,而无需创建高阶组件。
例如,可以使用 Effect Hook 解决前面提到的生命周期方法的问题,如下所示:
function ExampleComponent({ name }) {
**useEffect****(****() =>** **{**
**fetch****(****`http://my.api/****${name}****`****)**
**.****then****(...)**
**}, [name])**
// ...
}
这个 Effect Hook 将在组件挂载时自动触发,并且每当 name 属性发生变化时。
如前所述的包装地狱问题可以使用 Context Hooks 解决,如下所示:
const user = useContext(AuthenticationContext)
const language = useContext(LanguageContext)
const status = useContext(StatusContext)
如我们所见,通过使用 Hooks,我们可以保持代码的整洁和简洁,确保我们的代码易于阅读和维护。编写自定义 Hooks 也有助于在项目中重用应用程序逻辑。
现在我们知道了 Hooks 可以解决哪些问题,我们可以开始在实际中使用它们。然而,首先,我们需要快速设置我们的开发环境。
设置开发环境
在本书中,我们将使用 VS Code 作为我们的代码编辑器。请随意使用您偏好的任何编辑器,但请注意,您选择的编辑器中使用的扩展和配置的设置可能略有不同。
现在我们来安装 VS Code 和一些有用的扩展,然后继续设置我们开发环境所需的所有工具。
安装 VS Code 和扩展
在我们开始开发和设置其他工具之前,我们需要按照以下步骤设置我们的代码编辑器:
-
请从官方网站(截至编写时,网址为
code.visualstudio.com/)下载适用于您的操作系统的 VS Code。本书中将使用版本 1.97.2。 -
下载并安装应用程序后,打开它,您应该会看到以下窗口:
图 1.1 – 在 macOS 上 Visual Studio Code 的新安装
- 为了让事情更容易,我们将安装一些扩展,所以点击 Extensions 图标,这是截图左上角的第五个图标。
应该会打开一个侧边栏,您可以在顶部看到 Search Extensions in Marketplace。在此处输入扩展名称,然后点击 Install 来安装它。让我们先安装 ESLint 扩展:
图 1.2 – 在 Visual Studio Code 中安装 ESLint 扩展
-
确保安装以下扩展:
-
ESLint(由 Microsoft 提供)
-
Prettier – 代码格式化工具(由 Prettier 提供)
-
支持 JavaScript 和 Node.js 已经内置在 VS Code 中。
-
为这本书中制作的项目创建一个文件夹(例如,您可以将其命名为
Learn-React-Hooks-Second-Edition)。在这个文件夹内部,创建一个名为Chapter01_1的新文件夹。 -
在 VS Code 中打开空的
Chapter01_1文件夹。 -
如果出现一个对话框询问你信任此文件夹中文件的作者吗?,请选择信任父文件夹‘Learn-React-Hooks’中的所有文件的作者,然后点击是,我信任作者按钮。
图 1.3 – 允许 VS Code 在项目文件夹中执行文件
在您自己的项目中,您可以安全地忽略此警告,因为您可以确信这些项目中不包含恶意代码。当从不受信任的来源打开文件夹时,您可以按不,我不信任作者,仍然浏览代码。然而,这样做时,VS Code 的一些功能将被禁用。
我们现在已经成功设置了 VS Code,并准备好开始设置我们的项目!如果您从 GitHub 提供的代码示例中克隆了文件夹,也会弹出一个通知,告诉您找到了 Git 仓库。您可以简单地关闭它,因为我们只想打开Chapter01_1文件夹。
现在 VS Code 已经准备好了,让我们继续通过使用 Vite 设置一个新的项目。
使用 Vite 设置项目
对于这本书,我们将使用Vite来设置我们的项目,因为它是最受欢迎和最受欢迎的本地开发服务器,根据The State of JS 2024调查(2024.stateofjs.com/)。
Vite 还使得设置现代前端项目变得容易,同时如果需要,还可以稍后扩展配置。按照以下步骤使用 Vite 设置您的项目:
-
在 VS Code 菜单栏中,转到终端 | 新建终端以打开一个新的终端。
-
在终端内部,运行以下命令:
$ npm create vite@6.2.0 .
$符号表示这是一个需要输入到终端中的命令。将$符号之后的所有内容输入到终端中,并使用Return/Enter确认以运行命令。确保命令末尾有一个句点,以便在当前文件夹中创建项目,而不是创建一个新的文件夹。
为了确保即使新版本发布,本书中的说明仍然有效,我们将所有包固定到特定版本。请按照给定的版本执行说明。完成本书后,当你自己开始新项目时,你应该始终尝试使用最新版本,但请注意,可能需要进行一些更改才能使其工作。请查阅相应包的文档,并遵循从本书版本到最新版本的迁移路径。
-
当被问及是否应安装
create-vite时,只需输入y并按 Return/Enter 键继续。 -
如果被询问当前目录不为空,选择 删除现有文件并继续 选项,然后按 Return/Enter 确认。
-
当被要求输入包名时,通过按 Return/Enter 确认默认建议。
-
当被询问框架时,使用箭头键选择 React 并按 Return/Enter。
-
当被问及变体时,选择 JavaScript。
为了简单起见,并为了满足更广泛的受众,本书中我们只使用了纯 JavaScript。需要注意的是,如今 TypeScript 在许多项目中得到了广泛应用,所以你可能希望在将来的项目中考虑采用 TypeScript。然而,学习 TypeScript 超出了本书的范围。
-
编辑
package.json并确保dependencies和devDependencies的版本如下:"dependencies": { "react": "19.0.0", "react-dom": "19.0.0" }, "devDependencies": { "@eslint/js": "9.19.0", "@types/react": "19.0.8", "@types/react-dom": "19.0.3", "@vitejs/plugin-react": "4.3.4", "eslint": "9.19.0", "eslint-plugin-react": "7.37.4", "eslint-plugin-react-hooks": "5.0.0", "eslint-plugin-react-refresh": "0.4.18", "globals": "15.14.0", "vite": "6.1.0" } -
现在我们的项目已设置好,我们可以在终端中运行
npm install来安装依赖项。 -
之后,运行
npm run dev来启动开发服务器,如下截图所示:
图 1.4 – 使用 Vite 设置项目后的终端,在启动开发服务器之前
为了设置简单,我们直接使用了 npm。如果你更喜欢 yarn 或 pnpm,你可以分别运行 yarn create vite 或 pnpm create vite。
-
在终端中,你会看到一个 URL,告诉你你的应用正在运行的位置。你可以通过按住 Ctrl (Cmd 在 macOS 上) 并点击链接在浏览器中打开它,或者手动在浏览器中输入 URL。现在在浏览器中打开链接。
-
要测试你的应用是否是交互式的,点击带有文本 计数为 0 的按钮,每次点击它都应该增加计数。
图 1.5 – 使用 Vite 运行的我们的第一个 React 应用
Vite 的替代方案
Vite 的替代品是 webpack、Rollup 和 Parcel 等打包器。这些打包器高度可配置,但通常不提供出色的开发服务器体验。它们必须首先将所有我们的代码打包在一起,然后再将其提供给浏览器。相反,Vite 原生支持ECMAScript Module(ESM)标准。此外,Vite 启动时几乎不需要配置。Vite 的一个缺点是,用它配置某些更复杂的场景可能很困难。一个有希望的即将到来的打包器是 Rolldown (rolldown.rs);然而,在撰写本文时,它仍然非常新。
现在我们已经启动并运行了样板项目,让我们花些时间设置一些工具,这些工具将强制执行最佳实践和一致的代码风格。
设置 ESLint 和 Prettier 以强制执行最佳实践和代码风格
现在我们已经设置了 React 应用程序,我们将设置ESLint以使用 JavaScript 和 React 强制执行编码最佳实践。我们还将设置Prettier以强制执行代码风格并自动格式化我们的代码。
安装必要的依赖项
首先,我们将安装所有必要的依赖项。
-
在终端中,点击终端面板右上角的Split Terminal图标以创建一个新的终端面板(或者,在终端面板上右键单击并选择Split Terminal)。这将保持我们的应用程序运行,同时我们可以运行其他命令。
-
点击这个新打开的面板以将其聚焦。然后,输入以下命令来安装 Prettier 和 Prettier 的 ESLint 配置:
$ npm install --save-dev --save-exact prettier@3.5.1 eslint-config-prettier@10.0.1在
npm中使用--save-dev标志将那些依赖项保存为dev依赖项,这意味着它们将仅用于开发。它们不会被安装并包含在部署的应用程序中。--save-exact标志确保版本被固定为书中提供的确切版本。
依赖项安装完成后,我们需要配置 Prettier 和 ESLint。我们将从配置 Prettier 开始。
配置 Prettier
Prettier 将为我们格式化代码,并替换 VS Code 中 JavaScript 的默认代码格式化器。它将允许我们花更多的时间编写代码,在保存文件时自动为我们正确地格式化代码。按照以下步骤配置 Prettier:
-
在 VS Code 左侧侧边栏的文件列表下方(如果未打开,请点击Files图标)右键单击,然后点击**New file...**来创建一个新文件。将其命名为
.prettierrc.json(不要忘记文件名开头的点!)。 -
新创建的文件应自动打开;开始将以下配置写入其中。我们首先创建一个新的对象,并将
trailingComma选项设置为all,以确保跨越多行的对象和数组始终在末尾有逗号,即使是最后一个元素。这减少了通过 Git 提交更改时需要修改的行数:{ "trailingComma": "all", -
然后,我们将
tabWidth选项设置为两个空格:"tabWidth": 2, -
将
printWidth设置为每行 80 个字符,以避免代码中出现长行:"printWidth": 80, -
将
semi选项设置为false以避免在不必要的地方使用分号:"semi": false, -
最后,我们强制使用单引号而不是双引号:
"jsxSingleQuote": true, "singleQuote": true }这些 Prettier 设置只是编码风格约定的一个示例。当然,您可以自由调整以符合您的个人喜好。还有更多选项,所有这些都可以在 Prettier 文档中找到(
prettier.io/docs/en/options.html)。
配置 Prettier 扩展
现在我们已经有了 Prettier 的配置文件,我们需要确保 VS Code 扩展已正确配置以为我们格式化代码:
-
通过在 Windows/Linux 上转到文件 | 首选项... | 设置或在 macOS 上转到代码 | 设置... | 设置来打开 VS Code 设置。
-
在新打开的设置编辑器中,点击Workspace标签。这确保我们将所有设置保存在项目文件夹中的
.vscode/settings.json文件中。当其他开发者打开我们的项目时,他们也会自动使用这些设置。 -
搜索
editor format on save并勾选复选框以启用保存时格式化代码。 -
在列表中搜索
editor default formatter并选择Prettier - Code formatter。 -
要验证 Prettier 是否正常工作,打开
.prettierrc.json文件,在行首添加一些额外的空格,并保存文件。您应该注意到 Prettier 已重新格式化代码以符合定义的代码风格。它将缩进空格的数量减少到 2。
现在 Prettier 已经正确设置,我们不再需要手动格式化代码。您可以随时输入代码并保存文件,Prettier 会自动为您格式化!
创建 Prettier 忽略文件
为了提高性能并避免在不需要自动格式化的文件上运行 Prettier,我们可以通过创建 Prettier 忽略文件来忽略某些文件和文件夹。按照以下步骤操作:
-
在项目根目录中创建一个名为
.prettierignore的新文件,类似于我们创建.prettierrc.json文件的方式。 -
添加以下内容以忽略转译的源代码:
dist/
node_modules/文件夹会自动被 Prettier 忽略。
现在我们已经成功设置了 Prettier,我们将配置 ESLint 以强制执行编码最佳实践。
配置 ESLint
虽然 Prettier 关注代码的风格和格式,但 ESLint 关注实际的代码,避免常见的错误或不必要的代码。现在让我们来配置它:
-
打开自动创建的
eslint.config.js文件,并向其中添加以下导入:import prettierConfig from 'eslint-config-prettier' -
将文件滚动到末尾,并在数组末尾添加 Prettier 配置,如下所示:
'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, **prettierConfig,** ] -
此外,禁用
react/prop-types规则,如下所示:'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], **'****react/prop-types'****:** **'off'****,** }, }, prettierConfig, ]
自 React 19 以来,属性类型检查已被完全移除,并且将被静默忽略。向 props 添加类型检查的唯一方法是使用完整的类型检查解决方案,例如 TypeScript。由于我们在这本书中专注于学习带有 Hooks 的纯 React,因此使用 TypeScript 不在范围之内。然而,如果你还没有学习 TypeScript,我强烈建议你在完成这本书后自学 TypeScript。
- 保存文件,并在终端中运行
npx eslint src以运行代码检查器。你会看到没有输出,这意味着一切都被代码检查器成功检查,没有错误!
npx 命令允许我们在类似在 package.json 脚本中运行它们的环境中执行 npm 包提供的命令。它还可以运行远程包而无需永久安装。如果包尚未安装,它会询问你是否应该这样做。
添加一个新的脚本来运行我们的代码检查器
在上一节中,我们通过手动运行 npx eslint src 来调用代码检查器。我们现在将向 package.json 中添加一个 lint 脚本:
-
在终端中,运行以下命令以在
package.json文件中定义一个代码检查脚本:$ npm pkg set scripts.lint="eslint src" -
现在,在终端中运行
npm run lint。这应该会成功执行eslint src,就像之前使用npx eslint src一样:
图 1.6 – 代码检查器成功运行,没有错误
现在我们已经成功设置了我们的开发环境,让我们继续学习如何在实践中使用 React 类组件与 React Hooks!
示例代码
本节示例代码可在 Chapter01/Chapter01_1 文件夹中找到。请检查文件夹内的 README.md 文件,以获取设置和运行示例的说明。
React Hooks 入门
正如我们在本章前面所学,React Hooks 解决了许多问题,尤其是在大型网络应用程序中。Hooks 是在 React 16.8 中添加的,它们允许我们使用状态,以及各种其他 React 功能,而无需编写类。在本节中,我们将首先定义一个类组件,然后我们将使用 Hooks 将相同的组件编写为函数组件。然后我们将讨论 Hooks 的优点以及如何从类迁移到基于 Hooks 的解决方案。
从类组件开始
让我们先创建一个传统的 React 类组件,它允许我们输入一个名称;然后这个名称将在我们的应用中显示:
-
将
Chapter01_1文件夹复制到新的Chapter01_2文件夹中,如下所示:$ cp -R Chapter01_1 Chapter01_2在 macOS 上,运行带有大写
-R标志的命令很重要,而不是-r。-r标志对符号链接的处理方式不同,会导致node_modules/文件夹损坏。-r标志仅出于历史原因存在,不应在 macOS 上使用。始终优先使用-R标志。 -
在 VS Code 中打开新的
Chapter01_2文件夹。 -
删除
src/assets/文件夹及其所有内容。 -
删除
src/App.css和src/index.css文件。 -
打开
src/main.jsx文件,并删除以下导入:import './index.css' -
此外,将
App组件的导入从默认导入更改为命名导入,如下所示:import **{** App **}** from './App.jsx'在大多数情况下,使用命名导出/导入比使用默认导出/导入更可取。使用命名导出/导入在重构代码时更不容易出错。例如,让我们假设你有一个
Login组件,并将其复制粘贴到一个新的Register组件中,但忘记将组件重命名为Register。使用默认导入,仍然可以将其导入为Register,尽管组件内部称为Login。然而,当在 React 开发者工具中进行调试或试图在项目中找到该组件时,你会看到它命名为Login,这可能会造成混淆,尤其是在大型项目中。在处理函数时,使用命名导出甚至更有用,因为它允许你轻松地在不同的文件中移动它们。 -
打开
src/App.jsx文件,并从中删除所有现有代码。 -
接下来,我们按照以下方式导入 React:
import React from 'react' -
然后,我们开始定义一个类组件:
export class App extends React.Component { -
接下来,我们必须定义一个
constructor方法,在其中设置初始的state对象,它将是一个空字符串。在这里,我们还需要确保调用super(props),以便让React.Component构造函数了解props对象:constructor(props) { super(props) this.state = { name: '' } } -
现在,我们定义一个方法来设置
name变量,通过使用this.setState。由于我们将使用该方法处理文本字段的输入,我们需要使用evt.target.value从输入字段获取值:handleChange(evt) { this.setState({ name: evt.target.value }) } -
然后,我们定义
render方法,在其中我们将显示一个输入字段和名称:render() { -
要从
this.state对象中获取name变量,我们将使用解构:const { name } = this.state
上述语句相当于执行以下操作:
const name = this.state.name
-
然后,我们显示当前输入的
name状态变量:return ( <div> <h1>My name is: {name}</h1> -
我们显示一个输入字段,并将处理程序方法传递给它:
<input type='text' value={name} onChange={this.handleChange} /> </div> ) } }
如果我们现在运行此代码,在输入文本时会出现以下错误,因为将处理程序方法传递给onChange会改变this上下文:
Uncaught TypeError: Cannot read properties of undefined (reading 'setState')
或者,在某些浏览器上,你可能得到以下错误:
TypeError: undefined is not an object (evaluating 'this.setState')
-
因此,现在我们需要调整
constructor方法,并将我们的处理程序方法的this上下文重新绑定到类上。编辑src/App.jsx并在构造函数中添加以下行:constructor(props) { super(props) this.state = { name: '' } **this****.****handleChange** **=** **this****.****handleChange****.****bind****(****this****)** } -
通过打开终端(在 VS Code 中,选择终端 | 新终端菜单选项),并执行以下命令来运行开发服务器:
$ npm run dev -
在浏览器中打开开发服务器的链接,你应该会看到组件正在渲染。现在尝试输入一些文本,它应该可以工作!
或者,自 ES6 以来,可以使用箭头函数作为类方法来避免重新绑定this上下文。
最后,我们的组件工作正常了!正如你所见,要使状态处理在类组件中正常工作,需要编写大量的代码。我们还必须重新绑定this上下文;否则,我们的处理方法将无法工作。这并不直观,而且在开发过程中很容易忽略,导致开发者体验不佳。
示例代码
本节示例代码位于Chapter01/Chapter01_2文件夹中。请检查文件夹内的README.md文件,以获取设置和运行示例的说明。
使用 Hooks 代替
在使用传统的类组件创建我们的应用后,我们将使用 Hooks 来编写相同的应用。和之前一样,我们的应用将允许我们输入一个名字,然后我们在应用中显示这个名字。
只能在 React 函数组件中使用 Hooks。你无法在 React 类组件中使用 Hooks。
按照以下步骤开始:
-
将
Chapter01_2文件夹复制到新的Chapter01_3文件夹中,如下所示:$ cp -R Chapter01_2 Chapter01_3 -
在 VS Code 中打开新的
Chapter01_3文件夹。 -
打开
src/App.jsx文件,并删除其中的所有现有代码。 -
首先,我们按照以下方式导入
useStateHook:import { useState } from 'react' -
我们从函数定义开始。在我们的例子中,我们不传递任何参数,因为我们的组件没有任何属性:
export function App() {
下一步是从组件状态中获取name变量。然而,我们无法在函数组件中使用this.state。我们已经了解到 Hooks 只是 JavaScript 函数,但这究竟意味着什么呢?这意味着我们可以像使用任何其他 JavaScript 函数一样,简单地从函数组件中使用 Hooks!
要通过 Hooks 使用状态,我们调用useState(),并将初始状态作为参数。这个函数返回一个包含两个元素的数组:
-
当前状态
-
用于设置状态的设置函数
-
我们可以使用解构来将这些两个元素存储在单独的变量中,如下所示:
const [name, setName] = useState('')
之前的代码等同于以下代码:
const nameHook = useState('')
const name = nameHook[0]
const setName = nameHook[1]
-
现在,我们定义输入处理函数,其中我们使用了
setName设置函数:function handleChange(evt) { setName(evt.target.value) }
由于我们现在不处理类,因此不再需要重新绑定this。
-
最后,我们通过从函数中返回它来渲染用户界面:
return ( <div> <h1>My name is: {name}</h1> <input type='text' value={name} onChange={handleChange} /> </div> ) }
就这样 – 我们已经成功首次使用了 Hooks!正如你所见,useStateHook 是this.state和this.setState的简单替代品。
通过在终端中执行npm run dev并打开浏览器中的 URL 来运行我们的应用:
图 1.7 – 使用 Hooks 的我们的第一个 React 应用!
在使用类组件和函数组件实现相同的应用后,让我们比较一下解决方案。
示例代码
本节示例代码位于Chapter01/Chapter01_3文件夹中。请检查文件夹内的README.md文件,以获取设置和运行示例的说明。
比较解决方案
让我们比较我们的两种解决方案,看看类组件和函数组件使用 Hooks 之间的区别。
类组件
类组件利用 constructor 方法来定义状态,并且需要重新绑定 this 以便能够将处理方法传递给 input 字段。完整的类组件代码如下:
import React from 'react'
export class App extends React.Component {
constructor(props) {
super(props)
this.state = { name: '' }
this.handleChange = this.handleChange.bind(this)
}
handleChange(evt) {
this.setState({ name: evt.target.value })
}
render() {
const { name } = this.state
return (
<div>
<h1>My name is: {name}</h1>
<input type='text' value={name} onChange={this.handleChange} />
</div>
)
}
}
如我们所见,类组件需要大量的样板代码来初始化 state 对象和处理函数。
现在,让我们来看看函数组件。
带有 Hooks 的函数组件
函数组件利用 useState Hook,因此我们不需要处理 this 或 constructor 方法。完整的函数组件代码如下:
import { useState } from 'react'
export function App() {
const [name, setName] = useState('')
function handleChange(evt) {
setName(evt.target.value)
}
return (
<div>
<h1>My name is: {name}</h1>
<input type='text' value={name} onChange={handleChange} />
</div>
)
}
如我们所见,Hooks 使我们的代码更加简洁,并且更容易让开发者推理。我们不再需要担心内部的工作方式;我们可以简单地通过访问 useState 函数来使用状态!
Hooks 的优势
让我们再次回顾 React 的第一个原则:
声明式:我们不是告诉 React 如何去做事情,而是告诉它我们想要它做什么。因此,我们可以轻松地设计我们的应用程序,当数据发生变化时,React 将高效地更新和渲染正确的组件。
如我们在本章所学,Hooks 允许我们编写代码来告诉 React 我们想要什么。然而,对于类组件,我们需要告诉 React 如何去做事情。因此,Hooks 比类组件更加声明式,这使得它们更适合 React 生态系统。
Hooks 的声明式特性还意味着 React 可以对我们的代码进行各种优化,因为分析函数和函数调用比类和它们的复杂 this 行为更容易。此外,Hooks 使组件之间抽象和共享常见状态逻辑变得更加容易。通过使用 Hooks,我们可以避免使用渲染属性和高级组件。
我们可以看到,Hooks 不仅使我们的代码更加简洁,并且更容易让开发者推理,而且它们还使代码更容易为 React 优化。
迁移到 Hooks
现在,你可能想知道这是否意味着类组件已经过时,我们需要现在就将所有内容迁移到 Hooks。当然不是——Hooks 是完全可选的。您可以在某些组件中尝试 Hooks,而无需重写任何其他代码。React 团队目前也没有计划移除类。
目前没有必要急于将所有内容迁移到 Hooks。建议您逐步在某些组件中采用 Hooks,这些组件将最有用。例如,如果您有许多处理类似逻辑的组件,您可以将逻辑提取到 Hook 中。您还可以将带有 Hooks 的函数组件与类组件并行使用。
钩子具有向后兼容性,并提供了一个直接访问你已知的各种 React 概念的 API:props、state、context、refs 和 生命周期。此外,钩子还提供了结合这些概念的新方法,并以更好的方式封装它们的逻辑,从而不会导致包装地狱或类似问题。
钩子心态
钩子的主要目标是解耦状态逻辑和渲染逻辑。钩子允许我们在单独的函数中定义逻辑,并在多个组件之间重用它们。有了钩子,我们不需要更改组件层次结构来实现状态逻辑。不再需要定义一个单独的组件来为多个组件提供状态逻辑,我们只需使用一个钩子即可!
然而,钩子需要与传统 React 开发完全不同的心态。我们不再需要考虑组件的生命周期。相反,我们应该考虑数据流。例如,我们可以告诉钩子当某些 props 或其他钩子的值发生变化时触发。我们将在第四章 使用 Reducer 和 Effect 钩子 中了解更多关于这个概念。我们也不再需要根据生命周期方法来拆分组件。相反,我们可以使用钩子来处理常见功能,例如获取数据或设置数据订阅。
钩子规则
钩子非常灵活。然而,使用钩子有一些限制,我们应该始终牢记:
-
钩子只能在函数组件和其他钩子内部使用,不能在类组件或任意函数中使用
-
钩子定义的顺序很重要,需要保持一致;因此,我们不能在
if条件、循环或嵌套函数中放置钩子
幸运的是,Vite 已经为我们配置了一个 ESLint 插件,确保钩子规则不被违反。
我们将在本书的后续章节中更详细地讨论这些限制以及如何绕过它们。
摘要
在本书的第一章中,我们首先学习了 React 的基本原则以及它提供的组件类型。然后,我们继续学习关于类组件的常见问题,使用 React 的现有功能以及它们如何破坏基本原则。接下来,我们使用类组件和带有 Hooks 的函数组件实现了一个简单的应用程序,以便能够比较两种解决方案之间的差异。正如我们所发现的那样,带有 Hooks 的函数组件更适合 React 的基本原则;它们不会像类组件那样出现问题,并且使我们的代码更加简洁易懂!React 团队现在甚至推荐使用函数组件而不是类组件,使函数组件成为编写 React 代码的尖端方式。阅读本章后,React 和 React Hooks 的基本知识已经清楚。我们现在可以继续学习 Hooks 的详细内容。
在下一章中,我们将通过从头开始重新实现 State Hook 来深入了解其工作原理。通过这样做,我们将掌握 Hooks 的内部工作方式以及它们的局限性。之后,我们将使用 State Hook 创建一个小型博客应用程序!
问题
为了回顾我们在本章中学到的内容,尝试回答以下问题:
-
React 的三个基本原则是什么?
-
React 中有哪两种类型的组件?
-
React 中类组件有哪些问题?
-
使用高阶组件在 React 中会出现什么问题?
-
我们可以使用哪个工具来设置 React 项目,以及我们需要运行什么命令来使用它?
-
如果我们使用类组件遇到以下错误,我们应该做什么:
未捕获的类型错误:无法读取未定义的属性(读取'setState')? -
我们如何使用 Hooks 与 React 状态?
-
与类组件相比,使用带有 Hooks 的函数组件有哪些优势?
-
在更新 React 时,我们需要用带有 Hooks 的函数组件替换所有类组件吗?
进一步阅读
如果你对我们在本章中学到的概念感兴趣,请查看以下链接:
-
关于函数组件的信息:
react.dev/reference/react/Component -
React Hooks 的 RFC:
github.com/reactjs/rfcs/blob/main/text/0068-react-hooks.md -
模板字符串:
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals -
三元运算符:
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_operator
在 Discord 上了解更多
要加入本书的 Discord 社区——在那里您可以分享反馈、向作者提问以及了解新版本——请扫描下面的二维码:
packt.link/wnXT0
第二章:使用 State Hook
在了解了 React 的原则并对 Hooks 进行了介绍之后,我们现在将深入学习 State Hook。我们将从通过自己重新实现 State Hook 来了解其内部工作方式开始。这样做将使我们了解 Hooks 的限制以及它们存在的原因。然后,我们将学习可能的替代 Hook API 及其相关问题。最后,我们将学习如何解决由 Hooks 限制引起的常见问题。到本章结束时,您将知道如何使用 State Hook 在 React 中实现有状态的函数组件。
在本章中,我们将涵盖以下主要主题:
-
重新实现 State Hook
-
可能的替代 Hook API
-
使用 Hooks 解决常见问题
技术要求
Node.js 的相当新版本应该已经安装。Node 包管理器(npm)也需要安装(它应该包含在 Node.js 中)。有关如何安装 Node.js 的更多信息,请查看官方网站:nodejs.org/。
在这本书的指南中,我们将使用Visual Studio Code(VS Code),但任何其他编辑器都应该以类似的方式工作。有关如何安装 VS Code 的更多信息,请参阅官方网站:code.visualstudio.com。
在这本书中,我们使用以下版本:
-
Node.js v22.14.0
-
npmv10.9.2 -
VS Code v1.97.2
虽然安装新版本不应该有问题,但请注意,某些步骤在新版本上可能会有不同的工作方式。如果您在这本书提供的代码和步骤中遇到问题,请尝试使用提到的版本。
您可以在 GitHub 上找到本章的代码:github.com/PacktPublishing/Learn-React-Hooks-Second-Edition/tree/main/Chapter02。
强烈建议您自己编写代码。不要简单地运行书中提供的代码示例。自己编写代码对于正确学习和理解代码非常重要。然而,如果您遇到任何问题,您始终可以参考代码示例。
重新实现 State Hook
为了更好地理解 Hooks 在 React 内部的工作方式,我们将从头开始重新实现useState函数。然而,我们不会将其实现为一个实际的 React Hook,而是一个简单的 JavaScript 函数——只是为了了解 Hooks 实际上在做什么。
这次的重构实现并不完全等同于 React Hooks 在内部的工作方式。实际的实现方式相似,因此具有相似的约束。然而,实际的实现比我们在这里要实现的内容更为广泛。
我们现在将开始重新实现 State Hook:
-
通过执行以下命令将
Chapter01_3文件夹复制到新的Chapter02_1文件夹:$ cp -R Chapter01_3 Chapter02_1 -
在 VS Code 中打开新的
Chapter02_1文件夹。
首先,我们需要定义一个函数来(重新)渲染应用,我们可以使用它来模拟当 Hook 状态变化时的 React 重新渲染。如果我们使用实际的 React Hooks,这将在内部处理。
-
打开
src/main.jsx并删除以下代码:createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode>, )
替换为以下内容:
const root = createRoot(document.getElementById('root'))
export function renderApp() {
root.render(
<StrictMode>
<App />
</StrictMode>,
)
}
renderApp()
root for our React application to be rendered in. Then, we define a function to render the app into the root. Finally, we call the renderApp() function to initially render the app.
-
现在,打开
src/App.jsx文件并删除以下行:import { useState } from 'react'
将其替换为以下行:
import { renderApp } from './main.jsx'
-
现在,我们定义我们自己的
useState函数。正如我们已经知道的,useState函数接受initialState作为参数:function useState(initialState) { -
然后,我们定义一个值,我们将在这里存储我们的状态。最初,这个值将被设置为
initialState:let value = initialState -
接下来,我们定义
setState函数,我们将在这里设置新值,并强制重新渲染我们的应用:function setState(nextValue) { value = nextValue renderApp() } -
最后,我们将
value和setState函数作为一个数组返回:return [value, setState] } -
启动
dev服务器(保持运行)然后在浏览器中打开链接:$ npm run dev
如果你现在尝试在输入字段中输入文本,你会注意到当组件重新渲染时,状态被重置,因此无法在字段中输入任何文本。我们将在下一节中解决这个问题。
我们使用数组而不是对象的原因是我们通常想要重命名value和setState变量。使用数组可以通过解构轻松地重命名变量。例如,如果我们想要为username设置状态,我们可以这样做:
const [username, setUsername] = useState('')
虽然在对象中也可以通过解构进行重命名,但这会更冗长:
const { state: username, setState: setUsername } = useState('')
如我们所见,Hooks 是处理副作用(如设置有状态值)的简单 JavaScript 函数。
我们的 Hook 函数使用闭包来存储当前值。闭包是一个变量存在和存储的环境。在我们的情况下,函数提供了闭包,而value变量存储在这个闭包中。setState函数也是在同一个闭包中定义的,这就是为什么我们可以在该函数中访问value变量。在useState函数外部,除非我们从函数中返回它,否则我们无法直接访问value变量。
解决简单 Hook 实现中的问题
无法输入任何文本到输入字段的问题是由于每次组件渲染时都会重新初始化value变量,因为我们每次渲染组件时都会调用useState。
在接下来的部分,我们将通过使用全局变量然后将简单值转换为数组来解决此问题,这样我们就可以定义多个 Hook。
使用全局变量
正如我们所学的,value存储在由useState函数定义的闭包中。每当组件重新渲染时,闭包都会重新初始化,这意味着value变量将再次设置为initialState。为了解决这个问题,我们需要将value存储在函数之外的全局变量中。这样,value变量就会在函数的外部闭包中,这意味着当函数再次被调用时,value不会重新初始化。
我们可以定义全局变量如下:
-
首先,编辑
src/App.jsx并在useState函数定义上方添加以下行:**let** **value** function useState(initialState) { -
然后,删除函数定义中的以下第一行:
let value = initialState
用以下代码片段替换它:
if (value === undefined) {
value = initialState
}
- 再次尝试在输入字段中输入一些文本;你会看到我们的 Hook 函数现在可以正常工作了!
现在,我们的useState函数使用全局的value变量而不是在其闭包内定义value变量,因此当函数再次被调用时,它不会重新初始化。虽然我们的 Hook 函数目前运行良好,但如果我们要添加另一个 Hook,我们会遇到另一个问题:所有 Hook 都写入同一个全局value变量!让我们通过向我们的组件添加第二个 Hook 来更详细地看看这个问题。
定义多个 Hook
假设我们想要为用户的姓氏创建第二个字段。我们可以通过以下步骤实现:
-
编辑
src/App.jsx并从在App组件开始,在当前 Hook 之后定义一个新的 Hook:export function App() { const [name, setName] = useState('') **const** **[lastName, setLastName] =** **useState****(****''****)** -
然后,定义一个函数来处理姓氏的变化:
function handleLastNameChange(evt) { setLastName(evt.target.value) } -
然后,在第一个名字之后显示
lastName值:return ( <div> <h1>My name is: {name} **{lastName}**</h1> -
最后,添加另一个用于姓氏的
input字段:<input type='text' value={name} onChange={handleChange} /> **<****input****type****=****'text'****value****=****{lastName}****onChange****=****{handleLastNameChange}** **/>** -
现在尝试输入第一个名字和姓氏。
你会注意到我们重新实现的 Hook 函数使用相同的值来更新两个状态,所以我们总是同时更改两个字段。现在让我们尝试修复这个问题。
添加对多个 Hook 的支持
为了支持多个 Hook,我们需要存储一个 Hook 值的数组而不是单个全局变量。我们现在将按照以下步骤重构value变量:
-
编辑
src/App.jsx并删除以下代码行:let value
用以下代码片段替换它:
let values = []
let currentHook = 0
-
然后,编辑
useState函数的第一行,我们现在在values数组的currentHook索引处初始化值:function useState(initialState) { if (**values[currentHook]** === undefined) { **values[currentHook]** = initialState } -
我们还需要更新 setter 函数,以便只更新相应的状态值。在这里,我们需要首先将
currentHook值存储在一个单独的hookIndex变量中,因为currentHook值稍后会发生变化。这确保了在useState函数的闭包内创建了一个currentHook变量的副本。否则,useState函数将访问外层闭包中的currentHook变量,该变量会在每次调用useState时被修改:**let** **hookIndex = currentHook** function setState(nextValue) { **values[hookIndex]** = nextValue renderApp() } -
按如下方式编辑
useState函数的return语句:**const** **value = values[currentHook++]** return [**value**, setState] }
使用 values[currentHook++],我们将 currentHook 的当前值作为索引传递给 values 数组,然后增加 currentHook 的值。这意味着 currentHook 将在函数返回后增加。
如果我们想要首先增加一个值然后使用它,我们可以使用 arr[++indexToBeIncremented] 语法,它首先增加然后传递结果到数组。
-
当我们开始渲染我们的组件时,我们仍然需要重置
currentHook计数器。在组件定义后立即添加以下行:export function App() { **currentHook =** **0** -
再次尝试输入第一个名字和最后一个名字。
最后,我们简单重新实现的 useState Hook 成功了!以下截图突出了这一点:
图 2.1 – 我们自定义的 Hook 重新实现成功了
如我们所见,使用全局数组来存储我们的 Hook 值解决了我们在定义多个 Hook 时遇到的问题。
示例代码
本节的示例代码可以在 Chapter02/Chapter02_1 文件夹中找到。请检查文件夹内的 README.md 文件,以获取设置和运行示例的说明。
在解决我们自定义 Hook 实现中遇到的问题之后,让我们更多地了解 Hooks 的一般限制。
我们能否定义条件 Hook?
如果我们想要添加一个复选框来切换第一个名字字段的用法,让我们通过实现这样的复选框来找出答案:
-
将
Chapter02_1文件夹复制到一个新的Chapter02_2文件夹中,如下所示:$ cp -R Chapter02_1 Chapter02_2 -
在 VS Code 中打开新的
Chapter02_2文件夹。 -
编辑
src/App.jsx并向App组件添加一个新的 Hook,该 Hook 将存储复选框的状态:export function App() { currentHook = 0 **const** **[enableFirstName, setEnableFirstName] =** **useState****(****false****)** -
然后,调整
name状态的 Hook,使其仅在第一个名字被启用时使用:**// eslint-disable-next-line react-hooks/rules-of-hooks** const [name, setName] = **enableFirstName ?** useState('') **: [****''****,** **() =>** **{}]** const [lastName, setLastName] = useState('')
我们需要禁用 ESLint 这一行;否则,它会大声告诉我们不能有条件地使用 Hooks。出于演示目的,我想展示当你忽略这个警告时会发生什么。
我们还定义了一个回退到空字符串 ('') 和一个不执行任何操作的函数 (() => {}),当 Hook 未定义时。
-
接下来,定义一个用于更改复选框状态的处理器函数:
function handleEnableChange(evt) { setEnableFirstName(evt.target.checked) } -
最后,渲染复选框:
return ( <div> <h1> My name is: {name} {lastName} </h1> **<****input** **type****=****'checkbox'** **value****=****{enableFirstName}** **onChange****=****{handleEnableChange}** **/>** -
启动
dev服务器,然后在浏览器中打开链接:$ npm run dev
在这里,我们要么使用 Hook,要么如果第一个名字被禁用,则返回初始状态和一个空设置函数,这样编辑输入字段将不起作用。
如果我们现在尝试这段代码,我们会注意到编辑最后一个名字仍然可以工作,但编辑第一个名字则不行,这正是我们想要的。正如以下截图所示,现在只有编辑最后一个名字可以工作:
图 2.2 – 在勾选复选框之前的应用程序状态
当我们点击复选框时,会发生一些奇怪的事情:
-
复选框被勾选
-
名字输入字段被启用
-
现在姓氏字段的值是名字字段的值
我们可以在以下屏幕截图看到点击复选框的结果:
图 2.3 – 点击复选框后应用的状态
我们可以看到,现在姓氏状态在名字字段中。值被交换,因为 Hooks 的顺序很重要。正如我们从我们的实现中知道的那样,我们使用currentHook索引来找出每个 Hook 的状态存储位置。然而,当我们插入一个额外的 Hook 在两个现有 Hooks 之间时,顺序就会混乱。
在勾选复选框之前,values数组如下:
-
[false, ''] -
Hook 顺序:
enableFirstName,lastName
然后,我们在姓氏字段中输入了一些文本:
-
[false, 'Hook'] -
Hook 顺序:
enableFirstName,lastName
然后,我们点击了复选框,这激活了另一个 Hook:
-
[true, 'Hook', ''] -
Hook 顺序:
enableFirstName,name,lastName
如我们所见,在两个现有 Hooks 之间插入一个新的 Hook 会使name Hook 从下一个 Hook(lastName)中“窃取”状态,因为它现在具有lastName Hook 之前拥有的相同索引。现在,lastName Hook 没有值,这导致它设置初始值(一个空字符串)。
因此,切换复选框将姓氏字段的值放入名字字段,并使姓氏字段为空。
示例代码
本节示例代码位于Chapter02/Chapter02_2文件夹中。请检查文件夹内的README.md文件,以获取设置和运行示例的说明。
在了解到 Hooks 总是需要以相同的顺序调用之后,让我们将我们的自定义 Hook 实现与真正的 React Hooks 进行比较。
将我们的重新实现与真正的 Hooks 进行比较
我们简单的 Hook 实现已经让我们对 Hooks 的内部工作方式有了了解。然而,在现实中,Hooks 并不使用全局变量。相反,它们在 React 组件中存储状态。它们还内部处理 Hook 计数器,因此我们不需要在函数组件中手动重置计数。此外,真正的 Hooks 在状态变化时自动触发组件的重新渲染。然而,为了能够做到这一点,Hooks 需要从 React 函数组件中调用。React Hooks 不能在 React 外部或 React 类组件内部调用。
通过重新实现useState Hook,我们学到了以下内容:
-
Hooks 是访问 React 功能的函数
-
Hooks 处理跨渲染持续存在的副作用
-
Hook 定义的顺序很重要
最后一点尤为重要,因为它意味着我们不能有条件地定义 Hooks。我们应该始终在函数组件的开始处定义所有 Hook,并且永远不要将它们嵌套在if语句、三元运算符或类似的结构中。
因此,我们也学到了以下内容:
-
React Hooks 必须在 React 函数组件或其他 Hook 内部调用
-
React Hooks 不能在条件或循环中定义
由于我们学到了一些限制,React Hooks 还有一些额外的限制:
-
React Hooks 不能在条件
return语句之后定义 -
React Hooks 不能在事件处理程序中定义
-
React Hooks 不能在
try/catch/finally块内定义 -
React Hooks 不能在传递给
useMemo、useReducer和useEffect的函数中定义(我们将在本书中学习更多关于这三个 Hook 的内容,但请现在记住这个限制)
现在,我们将探讨一些替代的 Hook API,它们将允许条件性 Hook,但它们也有自己的缺点。
潜在的替代 Hook API
有时,定义条件性 Hook 或在循环中定义 Hook 会很好,但为什么 React 团队决定以这种方式实现 Hooks?有哪些替代方案?让我们通过探讨其中的一些方案来了解做出这一决策所涉及的权衡。
命名 Hook
我们可以给每个 Hook 起一个名字,然后将 Hook 存储在对象中而不是数组中。然而,这不会使 API 变得如此优雅,我们还需要始终为 Hook 考虑独特的名称:
// NOTE: Not the actual React Hook API
const [name, setName] = useState('nameHook', '')
此外,还有一些未解决的问题:当条件设置为false或从循环中移除一个项目时会发生什么?我们会清除 Hook 状态吗?如果我们不清除 Hook 状态,我们可能会造成内存泄漏。如果我们清除它,我们可能会无意中丢弃用户输入。
即使解决了这些问题,仍然存在名称冲突的问题。例如,如果我们创建了一个名为nameHook的 Hook,那么我们不能再在组件中调用任何其他名为nameHook的 Hook,否则将导致名称冲突。这种情况也适用于库中的 Hook 名称,因此我们需要确保避免与库定义的 Hook 发生名称冲突!
Hook 工厂
或者,我们可以创建一个 Hook 工厂函数,它内部使用Symbol来为每个 Hook 提供一个独特的键名:
function createUseState() {
const keyName = Symbol()
return function useState() {
// …use unique key name to handle hook state…
}
}
然后,我们可以这样使用工厂函数:
// NOTE: Not the actual React Hook API
const useNameState = createUseState()
export function App () {
const [name, setName] = useNameState('')
// …
}
然而,这意味着我们需要为每个 Hook 实例化两次:一次在组件外部,一次在函数组件内部。这增加了出错的可能性。例如,如果我们创建了两个 Hook 并复制粘贴样板代码,那么我们可能会在 Hook 的名称上犯错误,或者在使用组件内的 Hook 时犯错误。
这种方法也使得创建自定义 Hook 变得更加困难,迫使我们编写包装函数。此外,与调试简单函数相比,调试这些包装函数更加困难。
其他替代方案
对于 React Hooks,提出了许多替代的 API,但每个都存在类似的问题:要么使 API 更难使用,灵活性降低,更难调试,或者引入名称冲突的可能性。
最后,React 团队决定最简单的 API 是通过记录 Hook 被调用的顺序来跟踪 Hook。这种方法有其自身的缺点,例如无法有条件地调用 Hook 或在循环中调用。然而,这种方法使得创建自定义 Hook 非常容易,并且使用和调试都很简单。我们也不必担心 Hook 的命名、名称冲突或编写包装函数。最终的 Hook 方法让我们可以使用 Hook 就像使用任何其他函数一样!
现在我们已经了解了各种提案和最终的 Hook 实现,让我们学习如何解决由于选择官方 API 的限制而导致的常见问题。
解决 Hook 的常见问题
如我们所发现的,使用官方 API 实现 Hooks 也有其自身的权衡和限制。我们现在将学习如何克服这些常见问题,这些问题源于 React Hooks 的限制。
我们将探讨可以用来克服这两个问题的解决方案:
-
解决条件 Hook
-
在循环中解决 Hook
解决条件 Hook
那么,我们如何实现条件 Hook?我们不必使 Hook 有条件,我们只需总是定义 Hook 并在我们需要时使用它。如果这不是一个选项,我们需要拆分我们的组件,这通常总是更好的选择!
总是定义 Hook
对于简单的情况,例如我们之前遇到的第一个和最后一个名称示例,我们只需总是保持 Hook 定义,如下所示:
-
将
Chapter02_2文件夹复制到新的Chapter02_3文件夹中,如下所示:$ cp -R Chapter02_2 Chapter02_3 -
在 VS Code 中打开新的
Chapter02_3文件夹。 -
编辑
src/App.jsx并 删除 以下两行:// eslint-disable-next-line react-hooks/rules-of-hooks const [name, setName] = enableFirstName ? useState('') : ['', () => {}]
替换 如下:
const [name, setName] = useState('')
-
现在,我们需要将条件移动到第一个名称被渲染的地方:
return ( <div> <h1> My name is: **{enableFirstName ? name : ''}** {lastName} </h1>
如果你想重新添加第一个名称字段在未启用时甚至不能编辑的功能,只需向 <input> 字段添加以下属性:disabled={!enableFirstName}。
-
运行
dev服务器,然后在浏览器中打开链接:$ npm run dev
现在,我们的示例运行正常!总是定义 Hook 对于简单情况通常是一个好的解决方案。在更复杂的情况下,可能无法始终定义 Hook。在这种情况下,我们需要创建一个新的组件,在那里定义 Hook,然后有条件地渲染组件。
示例代码
本节的示例代码可以在 Chapter02/Chapter02_3 文件夹中找到。请检查文件夹内的 README.md 文件,了解如何设置和运行示例。
分离组件
解决条件 Hooks 的另一种方法是拆分一个组件成多个组件,然后有条件地渲染这些组件。例如,假设我们在用户登录后想要从数据库中获取用户信息。
我们不能这样做,因为使用 if 条件可能会改变 Hooks 的顺序:
function UserInfo({ username }) {
// NOTE: Do NOT do this
if (username) {
const info = useFetchUserInfo(username)
return <div>{info}</div>
}
return <div>Not logged in</div>
}
相反,我们必须为用户登录时创建一个单独的组件,如下所示:
// NOTE: Do this instead
function LoggedInUserInfo({ username }) {
const info = useFetchUserInfo(username)
return <div>{info}</div>
}
function UserInfo({ username }) {
if (username) {
return <LoggedInUserInfo username={username} />
}
return <div>Not logged in</div>
}
使用两个独立的组件来处理非登录和登录状态是有意义的,因为我们想坚持一个组件一个功能的原理。通常,如果我们坚持最佳实践,不能有条件 Hooks 并不是很大的限制。
解决循环中的 Hooks
有时候,你可能需要在循环中定义 Hooks – 例如,如果你有动态添加新输入字段的方法,并且需要为每个字段提供一个 State Hook。
要解决我们希望在循环中使用 Hooks 的问题,我们可以使用包含数组的单个 State Hook,或者再次拆分我们的组件。例如,假设我们想要显示所有在线的用户。
使用数组
我们可以简单地使用包含所有用户的数组,如下所示:
function OnlineUsers({ users }) {
const [userInfos, setUserInfos] = useState([])
// ... fetch & keep userInfos up to date ...
return (
<div>
{users.map((username) => {
const user = userInfos.find((u) => u.username === username)
return <UserInfo key={username} {...user} />
})}
</div>
)
}
然而,这并不总是有意义的。例如,我们可能不希望通过 OnlineUsers 组件来更新用户状态,因为我们必须从数组中选择正确的用户状态,然后修改数组。这可能可行,但相当繁琐。
拆分组件
一个更好的解决方案是使用 UserInfo 组件中的 Hook。这样,我们可以保持每个用户状态的最新,而无需处理数组逻辑:
function OnlineUsers({ users }) {
return (
<div>
{users.map((username) => (
<UserInfo key={username} username={username} />
))}
</div>
)
}
function UserInfo({ username }) {
const info = useFetchUserInfo(username)
// ... keep user info up to date ...
return <div>{info}</div>
}
如我们所见,使用一个组件来处理每个功能使我们的代码简单且简洁,同时也避免了 React Hooks 的限制。
摘要
在本章中,我们首先重新实现了 useState 函数,利用全局状态和闭包。然后我们了解到,为了支持多个 Hooks,我们需要使用数组来跟踪它们。然而,通过使用状态数组,我们被迫在函数调用之间保持 Hooks 的顺序一致。这种限制使得条件 Hooks 和循环中的 Hooks 变得不可能。然后我们学习了 Hook API 的潜在替代方案,它们的权衡以及为什么选择了最终的 API。最后,我们学习了如何解决由 Hooks 的限制引起的常见问题。现在我们对 Hooks 的内部工作和限制有了坚实的理解。在这个过程中,我们还深入了解了 State Hook。
在下一章中,我们将创建一个使用 State Hook 的博客应用程序,并学习如何组合多个 Hooks。
问题
为了回顾本章所学的内容,尝试回答以下问题:
-
在开发我们自己的
useStateHook 重新实现过程中,我们遇到了哪些问题?我们是如何解决这些问题的? -
为什么在 React 的钩子实现中不能使用条件钩子?
-
使用钩子时我们需要注意什么?
-
钩子替代 API 想法的常见问题有哪些?
-
我们如何实现条件钩子?
-
我们如何在循环中实现钩子?
进一步阅读
如果你对本章学到的概念感兴趣,想了解更多信息,请查看以下链接:
-
关于替代钩子 API 缺陷的更多信息:
overreacted.io/why-do-hooks-rely-on-call-order/ -
对替代钩子 API 的官方评论:
github.com/reactjs/rfcs/pull/68#issuecomment-439314884 -
关于钩子限制和规则的官方文档:
react.dev/reference/rules/rules-of-hooks -
关于
Symbol如何工作的更多信息:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
在 Discord 上了解更多
要加入本书的 Discord 社区——在那里你可以分享反馈,向作者提问,了解新版本——请扫描下面的二维码:
packt.link/wnXT0
第三章:使用 React Hooks 编写您的第一个应用程序
在深入了解 State Hook 之后,我们现在将利用它从头开始创建一个博客应用程序。在本章中,我们首先将学习如何以可扩展的方式结构化 React 应用程序。然后,我们将定义我们需要用到的组件,以覆盖博客应用程序的基本功能。最后,我们将使用 Hooks 将状态引入我们的应用程序!在本章中,我们还将了解 JSX 和各种 JavaScript 功能。在本章结束时,我们将拥有一个基本的博客应用程序,我们可以登录、注册和创建帖子。
本章将涵盖以下主题:
-
结构化 React 项目
-
实现静态 React 组件
-
使用 Hooks 实现有状态的组件
技术要求
应已安装一个相当新的 Node.js 版本。还需要安装 Node 包管理器 (npm)(它应随 Node.js 一起安装)。有关如何安装 Node.js 的更多信息,请访问官方网站:nodejs.org/。
在本书的指南中,我们将使用 Visual Studio Code (VS Code),但任何其他编辑器都应该以类似的方式工作。有关如何安装 VS Code 的更多信息,请参阅官方网站:code.visualstudio.com。
在本书中,我们使用以下版本:
-
Node.js v22.14.0
-
npmv10.9.2 -
Visual Studio Code v1.97.2
上列版本是本书中使用的版本。虽然安装较新版本通常不会有问题,但请注意,某些步骤在较新版本上可能会有所不同。如果您在使用本书提供的代码和步骤时遇到问题,请尝试使用提到的版本。
您可以在 GitHub 上找到本章的代码:github.com/PacktPublishing/Learn-React-Hooks-Second-Edition/tree/main/Chapter03。
强烈建议您亲自编写代码。不要简单地运行书中提供的代码示例。自己编写代码对于正确学习和理解代码非常重要。然而,如果您遇到任何问题,您始终可以参考代码示例。
结构化 React 项目
在了解 React 原则、如何使用 State Hook 以及 Hooks 内部工作原理之后,我们现在将利用真实的 State Hook 来开发一个博客应用程序。在本节中,我们将以允许我们以后扩展项目的方式结构化文件夹。
文件夹结构
项目可以有多种结构,不同的结构可能适合不同的项目。通常,创建一个src/文件夹来存放所有源代码是一个好主意,以区分资源和配置文件。在这个文件夹内,一种可能的组织方式是按特性分组文件。另一种流行的项目组织方式是按路由分组文件。对于某些项目,可能还需要按文件类型进一步分离,例如src/api/和src/components/。然而,对于我们的项目,我们主要将关注用户界面(UI)。因此,我们将在src/文件夹中按特性分组文件。
首先从一个简单的结构开始是个好主意,只有在你真正需要的时候才进行更深的嵌套。在项目开始时不要花太多时间思考文件结构,因为通常你事先不知道文件应该如何分组,而且它可能以后还会改变。然而,尽量避免使用通用的文件夹和文件名,如utils、common或shared。使用尽可能具体的术语,并在结构演变时进行扩展。
定义特性
我们首先必须考虑我们将在博客应用中实现哪些特性。至少,我们想要实现以下特性:
-
注册用户
-
登录/登出
-
查看单个帖子
-
创建新帖子
-
列出帖子
制定初始结构
从我们定义的特性中,我们可以抽象出一组功能组:
-
用户(注册、登录/登出)
-
帖子(创建、查看、列出)
我们现在可以保持非常简单,在src/文件夹中创建所有组件,而不需要任何嵌套。然而,由于我们已经对博客应用将需要的特性有了相当清晰的了解,我们可以提前制定一个文件夹结构:
-
src/ -
src/user/ -
src/post/
让我们现在设置初始的文件夹结构:
-
通过执行以下命令将
Chapter01_3文件夹复制到新的Chapter03_1文件夹:$ cp -R Chapter01_3 Chapter03_1 -
在 VS Code 中打开新的
Chapter03_1文件夹。 -
在
Chapter03_1文件夹内,创建新的src/user/和src/post/文件夹。
组件结构
React 中组件的理念是让每个组件处理单个任务或 UI 元素。我们应该尽量使组件尽可能细粒度,以便能够重用代码。如果我们发现自己正在从一个组件复制粘贴代码到另一个组件,可能将这个通用代码提取到一个可以重用的单独组件中是个好主意。
通常,在开发软件时,我们首先从 UI 原型开始。对于我们的博客,它看起来如下:
图 3.1 – 我们博客应用的初始原型
在拆分组件时,我们使用单一职责原则,该原则指出每个模块应该只负责功能的一个封装部分。
在原型中,我们可以在每个组件和子组件周围绘制方框,并给它们命名。请记住,每个组件应该只有一个职责。我们从这个应用的基本组件开始:
图 3.2 – 在我们的原型中绘制基本组件
我们绘制了一个Logout组件用于登出(在登出状态下将被Login/Register组件替换),一个CreatePost组件用于渲染创建新帖子的表单,以及一个Post组件用于实际的帖子。
现在我们已经绘制了基本组件,我们将查看哪些组件在逻辑上属于一组,因此形成一个组。为此,我们现在绘制容器组件,这是我们为了将组件组合在一起所需要的:
图 3.3 – 在我们的原型中绘制容器组件
我们绘制了一个PostList组件,用于将帖子分组,然后一个UserBar组件用于处理登录/登出和注册。最后,我们绘制了一个App组件,将其他所有内容组合在一起并定义我们应用的结构。
现在我们已经完成了 React 项目的结构化,我们可以继续实现静态组件。
实现静态组件
在我们通过 Hooks 向我们的博客应用添加状态之前,我们将使用静态 React 组件来模拟我们应用的基本功能。这样做意味着我们必须处理我们应用静态视图结构。
首先处理静态结构是有意义的,因为它将避免以后需要将动态代码移动到不同的组件中。此外,首先只处理 HTML(和 CSS)更容易——帮助我们快速开始项目。然后,我们可以继续实现动态代码和处理状态。
逐步进行,而不是一次性实现所有内容,这有助于我们快速开始新项目,而不必一次性思考太多,并且减少了我们以后需要重构的量!
实现与用户相关的静态组件
我们将从静态组件中最简单的功能开始,即实现与用户相关的功能。正如我们从我们的原型中看到的,我们在这里需要四个组件:
-
一个在用户尚未登录时将要显示的
Login组件 -
一个在用户尚未登录时也将要显示的
Register组件 -
一个在用户登录后将要显示的
Logout组件 -
一个
UserBar组件,它将根据用户的登录状态有条件地显示其他组件
我们将首先定义前三个组件,它们都是独立组件。最后,我们将定义依赖于其他组件的UserBar组件。
登录组件
首先,我们将定义Login组件,我们将展示两个字段:一个用户名字段和一个密码字段。此外,我们还将展示一个登录按钮。让我们开始吧:
-
在之前设置的
Chapter03_1文件夹内,为我们的组件创建一个新文件:src/user/Login.jsx。 -
在新创建的
src/user/Login.jsx文件中,定义一个组件,目前它不接受任何属性:export function Login() { -
渲染一个
<form>,防止表单的默认提交行为和刷新页面:return ( <form onSubmit={(e) => e.preventDefault()}>
这里,我们使用一个匿名函数(也称为箭头函数)来定义onSubmit处理程序。匿名函数的定义如下:
-
如果它们没有参数,我们可以写
() => { ... },而不是function () { ... } -
有参数的情况下,我们可以写
(arg1, arg2) => { ... },而不是function (arg1, arg2) { ... }
如果我们不使用括号{ },函数体中的语句的结果也将自动从函数返回,尽管这通常在事件处理程序中不是问题。
-
然后,渲染两个输入字段来输入用户名和密码,以及一个提交登录表单的按钮:
<label htmlFor='login-username'>Username: </label> <input type='text' name='username' id='login-username' /> <br /> <label htmlFor='login-password'>Password: </label> <input type='password' name='password' id='login-password' /> <br /> <input type='submit' value='Login' /> </form> ) }
使用语义化 HTML,如<form>和<label>,可以使你的应用对使用辅助软件(如屏幕阅读器)的人更容易导航。此外,当使用语义化 HTML 时,键盘快捷键,如通过按Enter/Return键提交表单,将自动工作。我们使用了htmlFor和id属性来确保屏幕阅读器知道标签属于哪个输入字段。id属性在整个页面中必须是唯一的,但对于name属性,只要在表单内是唯一的就足够了。
现在已经实现了静态的Login组件,让我们渲染它来看看它的样子。
渲染登录组件
按照以下步骤渲染Login组件:
-
首先,编辑
src/App.jsx并删除其中的所有现有代码。 -
然后,按照以下方式导入
Login组件:import { Login } from './user/Login.jsx' -
定义并导出
App组件,目前它只是简单地渲染Login组件:export function App() { return <Login /> }
如果我们只返回一个组件,我们可以在return语句中省略括号。而不是写return (<Login />),我们可以简单地写return <Login />。
-
通过打开终端(VS Code 中的终端 | 新终端菜单选项)并执行以下命令来运行
dev服务器:$ npm run dev -
在你的浏览器中打开 dev 服务器的链接,你应该会看到
Login组件正在被渲染。如果你更改代码,它应该会自动刷新,所以你可以在这个章节中一直运行 dev 服务器。
图 3.4 – 我们博客应用的第一组件:带有用户名和密码的登录
如我们所见,静态的Login组件在 React 中渲染良好。
注册组件
静态的Register组件将与Login组件非常相似,多了一个重复密码的字段。如果它们如此相似,有人可能会想到将它们合并为一个组件,并添加一个 prop 来切换额外字段。然而,在这种情况下,最好让每个组件只处理一个功能。稍后,我们将使用动态代码扩展静态组件;然后,Register和Login将具有截然不同的逻辑,我们需要再次将它们分开。
然而,让我们开始编写Register组件的代码:
-
创建一个新的
src/user/Register.jsx文件。 -
定义一个包含用户名和密码字段的表单,类似于
Login组件:export function Register() { return ( <form onSubmit={(e) => e.preventDefault()}> <label htmlFor='register-username'>Username: </label> <input type='text' name='username' id='register-username' /> <br /> <label htmlFor='register-password'>Password: </label> <input type='password' name='password' id='register-password' /> <br />请注意,你应该优先使用 CSS 进行间距设置,而不是使用
<br />HTML 标签。然而,在这本书中,我们专注于 UI 结构和与 Hooks 的集成,因此我们尽可能简单地使用 HTML。 -
接下来,添加一个重复密码字段:
<label htmlFor='register-password-repeat'>Repeat password: </label> <input type='password' name='password-repeat' id='register-password-repeat' /> <br /> -
最后,添加一个注册按钮:
<input type='submit' value='Register' /> </form> ) } -
再次,我们可以编辑
src/App.jsx来显示我们的新组件,如下所示:import { **Register** } from './user/**Register**.jsx' export function App() { return <**Register** /> }
如我们所见,Register组件看起来与Login组件非常相似,但多了一个字段,并且按钮上的文本不同。
注销组件
接下来,我们将定义Logout组件,该组件将显示当前登录用户的名称,以及一个注销按钮:
-
创建一个名为
src/user/Logout.jsx的新文件。 -
编辑
src/user/Logout.jsx文件,并定义一个接受username属性的组件:export function Logout({ username }) {
这里,我们使用解构从props对象中提取username键。React 将所有组件 prop 作为单个对象传递给函数的第一个参数。在第一个参数上使用解构类似于在类组件中执行const { username } = this.props。
-
在其中,返回一个表单,显示当前登录用户和一个注销按钮:
return ( <form onSubmit={(e) => e.preventDefault()}> Logged in as: <b>{username}</b> <input type='submit' value='Logout' /> </form> ) } -
我们现在可以将
Register组件替换为Logout组件在src/App.jsx中,以查看我们新定义的组件(不要忘记传递usernameprop 给它!):import { **Logout** } from './user/**Logout**.jsx' export function App() { return **<****Logout****username****=****'Daniel Bugl'** **/>** }
现在已经定义了Logout组件,我们可以继续编写UserBar组件。
用户栏组件
现在,是时候将我们的用户相关组件组合成一个UserBar组件了,我们将根据用户是否已经登录,有条件地显示Login和Register组件或Logout组件。
让我们开始实现UserBar组件:
-
创建一个新的
src/user/UserBar.jsx文件。 -
在其中,导入
Login、Logout和Register组件:import { Login } from './Login.jsx' import { Logout } from './Logout.jsx' import { Register } from './Register.jsx' -
定义
UserBar组件和一个username变量。目前,我们将其设置为静态值:export function UserBar() { const username = '' -
然后,我们检查用户是否已登录。如果用户已登录,我们显示
Logout组件,并将其username传递给它:if (username) { return <Logout username={username} /> } -
否则,我们显示
Login和Register组件。在这里,我们可以使用React.Fragment(简写语法:<>和</>)而不是<div>容器。这使我们的 UI 树保持清洁,因为组件将简单地并排渲染,而不是被另一个元素包裹:return ( <> <Login /> <hr /> <Register /> </> ) } -
编辑
src/App.jsx并显示UserBar组件,如下所示:import { **UserBar** } from './user/**UserBar**.jsx' export function App() { return **<****UserBar** **/>** }
如你所见,UserBar 组件成功渲染了 Login 和 Register 组件:
图 3.5 – 用户尚未登录时的 UserBar 组件
-
你可以尝试编辑静态的
username变量,看看它是否会渲染Logout组件。编辑src/user/UserBar.jsx并按照以下方式调整:export function UserBar() { const username = '`Daniel Bugl`'
进行此更改后,UserBar 组件将渲染 Logout 组件:
图 3.6 – 定义 username 后的 UserBar 组件
在本章的后面部分,我们将向我们的应用程序添加 Hooks,这样我们就可以动态地登录并改变状态,而无需编辑代码!
实现帖子
在实现所有用户相关组件后,我们现在可以继续在我们的博客应用程序中实现帖子。我们将定义以下组件:
-
一个用于显示单个帖子的
Post组件 -
一个用于创建新帖子的
CreatePost组件 -
一个
PostList组件用于显示所有帖子的列表
Post 组件
我们在创建原型时已经考虑了帖子应该包含哪些元素。帖子应该有一个标题、内容和作者(撰写帖子的用户)。
现在让我们来实现 Post 组件:
-
创建一个新的
src/post/Post.jsx文件。 -
在其中,以类似于原型的方式渲染所有属性:
export function Post({ title, content, author }) { return ( <div> <h3>{title}</h3> <div>{content}</div> <br /> <i> Written by <b>{author}</b> </i> </div> ) } -
和往常一样,我们可以通过编辑
src/App.jsx文件来测试我们的组件:import { **Post** } from './**post**/**Post**.jsx' export function App() { **return** **(** **<****Post** **title****=****'React Hooks'** **content****=****'The greatest thing since sliced bread!'** **author****=****'****Daniel Bugl'** **/>** **)** }
现在静态的 Post 组件已经实现,我们可以继续进行 CreatePost 组件的开发。
创建帖子组件
我们需要实现一个表单来创建新帖子。在这里,我们将 username 作为属性传递给组件,因为作者始终是当前登录的用户。然后,我们显示作者并提供一个标题输入字段和一个 <textarea> 元素用于博客帖子的内容。
现在让我们来实现 CreatePost 组件:
-
创建一个新的
src/post/CreatePost.jsx文件。 -
在其中,根据原型定义组件:
export function CreatePost({ username }) { return ( <form onSubmit={(e) => e.preventDefault()}> <div> Author: <b>{username}</b> </div> <div> <label htmlFor='create-title'>Title:</label> <input type='text' name='title' id='create-title' /> </div> <textarea name='content' /> <input type='submit' value='Create' /> </form> ) } -
和往常一样,我们可以通过编辑
src/App.jsx文件来测试我们的组件,如下所示:import { **CreatePost** } from './post/**CreatePost**.jsx' export function App() { return **<****CreatePost****username****=****'****Daniel Bugl'** **/>** }
如我们所见,CreatePost 组件渲染良好。我们现在可以继续进行 PostList 组件的开发。
帖子列表组件
在实现其他与帖子相关的组件后,我们现在可以实施我们博客应用最重要的部分:博客帖子的流。目前,流将简单地显示博客帖子的列表。
现在让我们开始实现PostList组件:
-
创建一个新的
src/post/PostList.jsx文件。 -
首先,我们导入
Fragment和Post组件:import { Fragment } from 'react' import { Post } from './Post.jsx' -
然后,我们定义接受一个
posts数组作为属性的PostList函数组件。如果posts未定义,我们默认将其设置为空数组:export function PostList({ posts = [] }) { -
接下来,我们使用
.map函数和展开语法来渲染所有帖子:return ( <div> {posts.map((post, index) => ( <Post {...post} key={`post-${index}`} /> ))} </div> ) }
我们为每个帖子返回<Post>组件,并将post对象的所有键作为属性传递给组件。我们通过使用展开语法来完成此操作,这具有与手动将对象的所有键作为属性列出相同的效果,如下所示:
<Post
title={post.title}
author={post.author}
content={post.content}
/>
如果我们在渲染元素列表,我们必须为每个元素提供一个唯一的key属性。React 使用这个key属性在数据发生变化时高效地计算两个列表之间的差异。使用唯一的 ID 作为key属性的最佳实践,例如数据库 ID,这样 React 可以跟踪列表中变化的项目。然而,在这种情况下,我们没有这样的 ID,所以我们简单地回退到使用索引。
我们使用了map函数,它将一个函数应用于数组的所有元素。这与使用for循环并存储所有结果类似,但更简洁、声明性更强,更容易阅读!作为使用map函数的替代方案,我们可以这样做:
let renderedPosts = []
let index = 0
for (let post of posts) {
renderedPosts.push(<Post {...post} key={`post-${index}`} />)
index++
}
return (
<div>
{renderedPosts}
</div>
)
然而,不建议在 React 中使用这种风格。
-
在原型中,每个博客帖子后面都有一个水平线。我们可以通过使用
Fragment而不添加额外的<div>容器元素来实现这一点,如下所示:{posts.map((post, index) => ( **<****Fragment****key****=****{****`****post-****${****index****}`}>** **<****Post** **{****...post****} />** **<****hr** **/>** **</****Fragment****>** ))}
使用Fragment而不是额外的<div>容器元素可以保持 DOM 树整洁并减少嵌套的数量。
key属性必须始终添加到在map函数中渲染的最高父元素。在这种情况下,我们必须将key属性从Post组件移动到Fragment。
-
再次,我们通过编辑
src/App.jsx文件来测试我们的组件:import { **PostList** } from './post/**PostList**.jsx' **const** **posts = [** **{** **title****:** **'React Hooks'****,** **content****:** **'The greatest thing since sliced bread!'****,** **author****:** **'Daniel Bugl'****,** **},** **{** **title****:** **'Using React Fragments'****,** **content****:** **'Keeping the DOM tree clean!'****,** **author****:** **'Daniel Bugl'****,** **},** **]** export function App() { return **<****PostList****posts****=****{posts}** **/>** }
现在,我们可以看到我们的应用列出了我们在posts数组中定义的所有帖子。
如我们所见,通过PostList组件列出多个帖子工作得很好。现在我们可以继续组装应用。
组装应用
在实现所有组件以重现原型后,我们只需在App组件中将所有内容组合在一起。然后,我们就成功重现了原型!
让我们从修改App组件并组装我们的应用开始:
-
编辑
src/App.jsx并删除所有当前代码。 -
首先,导入
UserBar、CreatePost和PostList组件:import { UserBar } from './user/UserBar.jsx' import { CreatePost } from './post/CreatePost.jsx' import { PostList } from './post/PostList.jsx' -
然后,为应用定义一些模拟数据:
const username = 'Daniel Bugl' const posts = [ { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl', }, { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl', }, ] -
接下来,定义
App组件并返回一个带有一些填充的容器:export function App() { return ( <div style={{ padding: 8 }}> -
现在,渲染
UserBar和CreatePost组件,并将username属性传递给CreatePost组件:<UserBar /> <br /> <CreatePost username={username} /> -
最后,显示
PostList组件,并将posts属性传递给它:<hr /> <PostList posts={posts} /> </div> ) }
保存文件后,浏览器应自动刷新,我们现在可以看到完整的 UI:
图 3.7 – 根据原型完全实现我们的静态博客应用程序
如我们所见,现在,我们之前定义的所有静态组件都在一个 App 组件中一起渲染。
示例代码
本节示例代码位于 Chapter03/Chapter03_1 文件夹中。请检查文件夹内的 README.md 文件,了解如何设置和运行示例。
我们的应用程序现在看起来就像原型一样,所以,我们现在可以继续使用 Hooks 来使所有组件动态化。
使用 Hooks 实现状态化组件
现在我们已经实现了应用程序的静态结构,我们将添加状态 Hooks 来处理状态和动态交互!
首先,让我们为状态实现创建一个新的文件夹:
-
将
Chapter03_1文件夹复制到新的Chapter03_2文件夹中,如下所示:$ cp -R Chapter03_1 Chapter03_2 -
在 VS Code 中打开新的
Chapter03_2文件夹。
为用户功能添加 Hooks
为了添加用户功能的 Hooks,我们需要将静态的 username 变量替换为一个 Hook。然后,我们需要在登录、注册和注销时调整值。
调整用户栏
当我们创建 UserBar 组件时,我们静态地定义了一个 username 变量。我们现在将用状态 Hook 来替换它!
让我们开始修改 UserBar 组件,使其动态化:
-
编辑
src/user/UserBar.jsx并导入useStateHook,如下所示:import { useState } from 'react' -
删除 以下代码行:
const username = 'Daniel Bugl'
替换 它为一个使用空用户名作为默认值的 State Hook:
const [username, setUsername] = useState('')
-
然后,将
setUsername函数传递给Logout组件:if (username) { return <Logout username={username} **setUsername****=****{setUsername}** /> }
为了简化并更容易跟踪状态处理的位置,我们将直接从状态 Hook 将 username 和 setUsername 函数传递到其他组件。在实际项目中,最好使用特定的名称来命名处理程序,例如 onLogout。这减少了组件之间的耦合。
-
此外,将
setUsername函数分别传递给Login和Register组件:return ( <> <Login **setUsername****=****{setUsername}** /> <hr /> <Register **setUsername****=****{setUsername}** /> </> ) }
现在,UserBar 组件可以动态地设置用户名。然而,我们仍然需要修改其他组件以添加处理程序。
-
编辑
src/user/Logout.jsx并定义一个handleSubmit函数,如下所示:export function Logout({ username**, setUsername** }) { **function****handleSubmit****(****e****) {** **e.****preventDefault****()** **setUsername****(****''****)** **}**在 React 19 中,表单操作被引入作为一种处理表单提交的高级方式。我们将在第七章中学习更多关于表单操作的内容,使用 Hooks 处理表单。在本章中,我们将专注于使用 State Hook 和传统的使用
onSubmit处理函数处理表单的方式。 -
然后,用新定义的函数替换现有的
onSubmit处理程序:return ( <form onSubmit={**handleSubmit**}> -
编辑
src/user/Login.jsx并定义一个handleSubmit函数,如下所示:export function Login(**{ setUsername }**) { **function****handleSubmit****(****e****) {** **e.****preventDefault****()** **const** **username = e.****target****.****elements****.****username****.****value** **setUsername****(username)** **}** return ( <form onSubmit={**handleSubmit**}>
如我们所见,我们可以通过使用e.target.elements直接访问表单中username字段的值。form元素的键等同于<input>元素的name属性。
-
编辑
src/user/Register.jsx并定义一个handleSubmit函数,如下所示:export function Register(**{ setUsername }**) { **function****handleSubmit****(****e****) {** **e.****preventDefault****()** **const** **username = e.****target****.****elements****.****username****.****value** **setUsername****(username)** **}** return ( <form onSubmit={**handleSubmit**}>
现在,你可以尝试注册、登录和登出,并查看状态在组件间如何变化。
添加验证
在尝试login和register功能时,你可能已经注意到没有进行验证。对于简单的验证,如必填字段,我们可以直接使用 HTML 功能。HTML 验证将阻止用户提交表单,如果字段无效,会弹出一个提示告诉用户哪里出了问题。然而,对于更复杂的验证,如检查重复密码是否相同,我们需要使用 State Hook 来跟踪表单的错误状态。
让我们开始实现验证:
-
编辑
src/user/Login.jsx并给以下input字段添加required属性:<input type='text' name='username' id='login-username' **required** /> … <input type='password' name='password' id='login-password' **required** /> -
编辑
src/user/Register.jsx并添加required属性:<input type='text' name='username' id='register-username' **required** /> … <input type='password' name='password' id='register-password' **required** /> … <input type='password' name='password-repeat' id='register-password-repeat' **required** /> -
在
src/user/Register.jsx文件中,也导入useState函数:import { useState } from 'react' -
然后,添加一个新的 State Hook 来跟踪错误状态:
export function Register({ setUsername }) { **const** **[invalidRepeat, setInvalidRepeat] =** **useState****(****false****)**
这种状态被称为局部状态,因为它只需要在一个组件内使用。
-
在
handleSubmit函数中,检查password和password-repeat字段是否相同。如果不相同,设置错误状态并从函数中返回:function handleSubmit(e) { e.preventDefault() **if** **(** **e.****target****.****elements****.****password****.****value** **!==** **e.****target****.****elements****[****'password-repeat'****].****value** **) {** **setInvalidRepeat****(****true****)** **return** **}**
如果不满足某些条件,函数的早期返回通常比嵌套if语句更可取。早期返回使函数易于阅读,并避免代码意外执行的问题。
-
在
if语句之后,如果密码相同,重置错误状态并处理注册:**setInvalidRepeat****(****false****)** const username = e.target.elements.username.value setUsername(username) } -
在表单末尾,在注册按钮之前,如果错误状态被触发,我们插入一条错误信息:
<br /> **{invalidRepeat && (** **<****div****style****=****{{****color:** **'****red****' }}>****Passwords must** **match.****</****div****>** **)}** <input type='submit' value='Register' /> </form>
如果我们现在尝试注册但密码没有正确重复,我们可以看到以下错误信息:
图 3.8 – 使用 Hooks 实现的验证和错误信息
现在我们已经成功实现了验证,我们可以继续将用户名传递给CreatePost组件。
将用户传递给 CreatePost
如您可能已经注意到的,CreatePost组件仍然使用硬编码的用户名。为了能够在那里访问用户名,我们需要将钩子从UserBar组件移动到App组件中:
-
编辑
src/user/UserBar.jsx并剪切/删除以下钩子定义:export function UserBar() { **const** **[username, setUsername] =** **useState****(****''****)** -
然后,调整函数定义以接受这两个属性:
export function UserBar(**{ username, setUsername }**) { -
删除以下
useState导入:import { useState } from 'react' -
现在,编辑
src/App.jsx并从那里导入useState函数:import { useState } from 'react' -
删除以下代码行:
const username = 'Daniel Bugl' -
在
App函数组件内部,添加我们之前移除的钩子:export function App() { **const** **[username, setUsername] =** **useState****(****''****)**
这种状态被称为全局状态,因为它在整个博客应用中的多个组件中都需要,这也是为什么我们将状态钩子移动到App组件中的原因。
-
然后,将
username值和setUsername函数传递给UserBar组件:return ( <div style={{ padding: 8 }}> <UserBar **username****=****{username}****setUsername****=****{setUsername}** />在第五章《实现 React 上下文》中,我们将学习一个更好的解决方案来将登录状态提供给其他组件。现在,我们只是将值和函数传递下去。
-
最后,确保
CreatePost组件仅在用户登录时渲染(username已定义):<br /> **{username &&** <CreatePost username={username} />**}**
现在用户功能已完全实现,我们可以继续使用钩子来实现帖子功能!
为帖子功能添加钩子
在实现了用户功能之后,我们现在将实现帖子的动态创建。我们首先调整App组件,然后修改CreatePost组件以能够插入新帖子。
调整App组件
与username状态类似,我们将在App组件中定义posts作为全局状态,并从那里提供给其他组件。
让我们开始调整App组件:
-
编辑
src/App.jsx并将当前的posts数组重命名为defaultPosts:const **defaultPosts** = [ { title: 'React Hooks', content: 'The greatest thing since sliced bread!', author: 'Daniel Bugl', }, { title: 'Using React Fragments', content: 'Keeping the DOM tree clean!', author: 'Daniel Bugl', }, ] -
然后,在
App函数内部定义一个新的posts状态钩子:export function App() { **const** **[posts, setPosts] =** **useState****(defaultPosts)** -
现在,将
setPosts作为属性传递给CreatePost组件:{username && ( <CreatePost username={username} **setPosts****=****{setPosts}** /> )}
在将状态提供给CreatePost组件后,让我们继续调整它。
调整CreatePost组件
现在,我们需要使用setPosts函数在按下创建按钮时插入一个新的帖子,如下所示:
-
编辑
src/post/CreatePost.jsx并调整函数定义以接受setPosts属性:export function CreatePost({ username**, setPosts** }) { -
接下来,定义一个
handleSubmit函数,在其中我们首先收集所有需要的值:function handleSubmit(e) { e.preventDefault() const form = e.target const title = form.elements.title.value const content = form.elements.content.value const newPost = { title, content, author: username }
在这里,我们将{ title: title }对象赋值简写为{ title },它们具有相同的效果。
-
然后,我们将新帖子插入到数组中:
setPosts((posts) => [newPost, ...posts])
在这里,我们使用一个函数来获取状态钩子的当前值,然后返回一个新值,其中包含插入到数组中的新帖子。
-
最后,我们将重置表单以清除所有输入字段:
form.reset() } -
我们仍然需要将新定义的函数分配给
onSubmit处理程序,如下所示:return ( <form onSubmit={**handleSubmit**}>
现在,我们可以登录并创建一个新的帖子,它将被插入到动态流的开始处!
图 3.9 – 使用 Hooks 插入新帖子后的我们的博客应用的第一版
示例代码
本节示例代码可在Chapter03/Chapter03_2文件夹中找到。请检查文件夹内的README.md文件,以获取设置和运行示例的说明。
摘要
在本章中,我们从零开始开发了自己的博客应用!我们从一个原型开始,然后创建了静态组件来模拟它。之后,我们实现了 Hooks 以允许动态行为。在整个章节中,我们学习了如何使用 Hooks 处理本地和全局状态。此外,我们学习了如何使用多个 Hooks,以及在哪些组件中定义 Hooks 和存储状态。我们还学习了如何解决常见用例,例如表单验证和提交。
在下一章,第四章,使用 Reducer 和 Effect 钩子,我们将学习 Reducer 钩子,它使我们能够更容易地处理某些状态变化。此外,我们还将学习 Effect 钩子,它允许我们运行具有副作用代码。
问题
为了回顾本章学到的内容,尝试回答以下问题:
-
在 React 中,有哪些好的文件夹结构方式?
-
在拆分 React 组件时,我们应该使用哪个原则?
-
map函数的作用是什么? -
解构是如何工作的,我们何时使用它?
-
扩展运算符是如何工作的,我们何时使用它?
-
我们如何处理表单验证和提交?
-
应该在哪里定义本地状态钩子?
-
什么是全局状态?
-
应该在哪里定义全局状态钩子?
进一步阅读
如果你对本章学到的概念有更多兴趣,请查看以下链接:
- 思考 React的官方文档:
react.dev/learn/thinking-in-react
在 Discord 上了解更多
要加入这本书的 Discord 社区——在那里你可以分享反馈,向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/wnXT0