React 挂钩学习指南第二版(三)
原文:
zh.annas-archive.org/md5/e3f80e0bbd9c0adfcf30deda2265e9fb译者:飞龙
第七章:使用 Hooks 处理表单
在上一章中,我们学习了如何使用 Hooks 进行数据获取和 React Suspense 在等待数据加载完成时显示回退。
在本章中,我们将学习如何使用 Hooks 来处理 React 中的表单和表单状态。我们之前已经为 CreatePost 组件实现了一个表单。然而,我们不是手动处理表单提交,而是可以使用 React 表单操作,这不仅使处理表单提交变得更容易,还允许我们使用访问表单状态的 Hooks。此外,我们还将学习如何使用 乐观钩子 来实现乐观更新,即在服务器端完成处理之前,在客户端显示初步结果。
本章将涵盖以下主题:
-
使用 Action 状态 Hook 处理表单提交
-
模拟 阻塞 UI
-
使用 过渡钩子 避免阻塞 UI
-
使用 乐观钩子 实现乐观更新
技术要求
应已安装一个相当新的 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/Chapter07
强烈建议您自己编写代码。不要简单地运行书中提供的代码示例。自己编写代码对于正确学习和理解代码非常重要。然而,如果您遇到任何问题,您始终可以参考代码示例。
使用 Action 状态 Hook 处理表单提交
React 19 引入了一个名为 Form Actions 的新功能。正如我们在前面的章节中看到的,在 Web 应用程序中,对用户操作进行数据变更是一个常见的用例。通常,这些数据变更需要发起 API 请求并处理响应,这意味着要处理加载和错误状态。例如,当我们创建 CreatePost 组件时,我们创建了一个表单,在提交时将新帖子插入到数据库中。在这种情况下,React Query 已经帮助我们很多,通过简化加载和错误状态。然而,使用 React Form Actions 现在有一个原生的方法来处理这些状态,通过使用 Action State Hook。
介绍 Action State Hook
Action State Hook 定义如下:
const [state, action, isPending] = useActionState(actionFn, initialState)
让我们稍微分解一下,以便更好地理解它。要定义一个 Action State Hook,我们需要至少提供一个函数作为参数。这个函数将在表单提交时被调用,并且具有以下签名:
function actionFn(currentState, formData) {
动作函数将动作的当前状态作为第一个参数,将表单数据(作为一个 FormData 对象)作为第二个参数。动作函数返回的任何内容都将成为 Action State Hook 的新状态。
FormData API 是一个用于表示表单字段及其值的 Web 标准。它可以用来处理表单提交并通过网络发送,例如使用 fetch()。它是一个可迭代的对象(可以使用 for … of 循环迭代)并提供 getter 和 setter 函数来访问值。更多信息可以在这里找到:developer.mozilla.org/en-US/docs/Web/API/FormData。
此外,还可以为 Action State Hook 提供一个 initialState。
该 Hook 然后返回动作的当前状态,动作本身(将被传递到 <form> 元素),以及 isPending 状态,以检查动作是否当前正在挂起(当 actionFn 正在执行时)。
使用 Action State Hook
现在,让我们开始重构 CreatePost 组件以使用 Action State Hook:
-
通过执行以下命令将
Chapter06_4文件夹复制到新的Chapter07_1文件夹:$ cp -R Chapter06_4 Chapter07_1 -
在 VS Code 中打开新的
Chapter07_1文件夹。 -
编辑
src/components/post/CreatePost.jsx并导入useActionState函数:import { useContext, **useActionState** } from 'react' -
在 CreatePost 组件内部,删除 整个
handleSubmit函数。 -
替换 为以下 Action State Hook:
const [error, submitAction, isPending] = useActionState(
在这种情况下,我们将使用动作的 state 来存储错误状态。如果有错误,我们将从动作函数中返回它。否则,我们返回 nothing,因此错误状态是 undefined。
-
定义动作函数,如下所示:
async (currentState, formData) => {
在这种情况下,我们不会使用传递给函数的 currentState,但我们仍然需要定义它,因为我们需要第二个参数来获取 formData。
-
现在,通过使用
FormDataAPI 从表单中获取标题和内容:const title = formData.get('title') const content = formData.get('content')
FormData API 使用 name 属性来识别输入字段。
-
接下来,创建帖子对象并调用突变:
const newPost = { title, content, author: username, featured: false } try { await createPostMutation.mutateAsync(newPost)
由于我们现在有一个 async 函数,我们可以使用 mutation 中的 mutateAsync 方法来能够 await 响应。
-
如果发生错误,则返回它:
} catch (err) { return err } }, )
我们不再需要手动重置表单。当使用表单操作时,一旦表单操作函数成功完成,表单中的所有未受控字段将自动重置。
-
调整
<form>元素以将action传递给它而不是onSubmit处理程序:return ( <form **action****=****{submitAction}**> -
调整提交按钮和错误信息,如下所示:
<input type='submit' value='Create' **disabled****=****{isPending}** /> {**error** && <div style={{ color: 'red' }}>{**error**.toString()}</div>} -
按照以下方式启动应用:
$ npm run dev -
在博客应用中登录并创建一篇新文章,你会看到它和之前一样工作,但现在我们正在使用 Action State Hook 来处理表单提交!
在学习如何使用 Action State Hook 处理表单提交之后,让我们继续学习关于阻塞 UI 的内容。
示例代码
本节示例代码可在 Chapter07/Chapter07_1 文件夹中找到。请检查文件夹内的 README.md 文件,了解如何设置和运行示例。
模拟阻塞 UI
在我们学习关于 Transition Hook 之前,让我们首先介绍它试图解决的问题:阻塞 UI。当某些组件计算密集时,渲染它们可能会导致整个用户界面无响应。这可能会导致糟糕的用户体验,因为用户在组件渲染时无法做任何事情。
我们现在将在我们的博客中实现一个评论部分来模拟阻塞 UI。
实现一个(故意慢的)Comment 组件
我们首先实现一个 Comment 组件,我们故意让它变慢以模拟计算密集型组件。
让我们开始实现 Comment 组件:
-
通过执行以下命令将
Chapter07_1文件夹复制到新的Chapter07_2文件夹:$ cp -R Chapter07_1 Chapter07_2 -
在 VS Code 中打开新的
Chapter07_2文件夹。 -
创建一个新的
src/components/comment/文件夹。 -
创建一个新的
src/components/comment/Comment.jsx文件。在其中,定义并导出一个Comment组件,该组件接受content和author属性:export function Comment({ content, author }) { -
在组件中,我们通过延迟渲染 1ms 来模拟计算密集型操作:
let startTime = performance.now() while (performance.now() - startTime < 1) { // do nothing for 1 ms } -
现在,按照以下方式渲染评论:
return ( <div style={{ padding: '0.5em 0' }}> <span>{content}</span> <i> ~ {author}</i> </div> ) }
实现一个 CommentList 组件
现在,我们将实现一个 CommentList 组件,它将渲染 1000 条评论:
-
创建一个新的
src/components/comment/CommentList.jsx文件。 -
在其中,导入
Comment组件:import { Comment } from './Comment.jsx' -
然后,定义并导出一个
CommentList组件,它将生成 1000 条评论:export function CommentList() { const comments = Array.from({ length: 1000 }, (_, i) => ({ id: i, content: `Comment #${i}`, author: 'test', })) -
渲染评论:
return ( <div> {comments.map((comment) => ( <Comment {...comment} key={comment.id} /> ))} </div> ) }
实现 CommentSection 组件
最后,我们将实现一个 CommentSection 组件,它将允许我们通过按按钮来显示/隐藏帖子的评论。
让我们开始实现 CommentSection 组件:
-
创建一个新的
src/components/comment/CommentSection.jsx文件 -
在其中,从 React 导入
useState函数和CommentList组件:import { useState } from 'react' import { CommentList } from './CommentList.jsx' -
接下来,定义并导出
CommentSection组件,在其中我们定义一个 State Hook 来切换评论列表的开启和关闭:export function CommentSection() { const [showComments, setShowComments] = useState(false) -
然后,定义一个
handleClick函数,该函数将切换评论列表:function handleClick() { setShowComments((prev) => !prev) } -
渲染一个按钮,并条件性地渲染
CommentList组件:return ( <div> <button onClick={handleClick}> {showComments ? 'Hide' : 'Show'} comments </button> {showComments && <CommentList />} </div> ) } -
最后,编辑
src/components/post/Post.jsx并在那里导入CommentSection组件:import { CommentSection } from '@/components/comment/CommentSection.jsx' -
在帖子的末尾渲染它,如下所示:
<i> Written by <b>{author}</b> </i> **<****br** **/>** **<****br** **/>** **<****CommentSection** **/>** </div> ) }
测试模拟的阻塞 UI
现在,我们可以测试评论部分,看看它如何导致 UI 堵塞。请按照以下步骤操作:
-
按照以下方式运行项目:
$ npm run dev -
通过访问
http://localhost:5173/在浏览器中打开前端。 -
点击其中一个 显示评论 按钮。
-
你会看到在按下按钮后,整个 UI 变得无响应。尝试按下其他 显示评论 按钮之一——它不起作用。
如我们所见,渲染计算密集型的组件可能会导致整个 UI 变得无响应。为了解决这个问题,我们需要使用过渡——我们将在下一节中学习。
示例代码
本节的示例代码可以在 Chapter07/Chapter07_2 文件夹中找到。请检查文件夹内的 README.md 文件,以获取设置和运行示例的说明。
使用 Transition Hook 避免阻塞 UI
Transition Hook 允许你在不阻塞 UI 的情况下通过更新状态来处理异步操作。这对于渲染计算密集型的组件树特别有用,例如渲染标签及其(可能复杂的)内容,或者当制作客户端路由器时。Transition Hook 具有以下签名:
const [isPending, startTransition] = useTransition()
可以使用 isPending 状态来处理加载状态。startTransition 函数允许我们传递一个函数来启动过渡。这个函数需要是同步的。当函数内部触发的更新(例如,设置状态)正在执行并且它们对组件的影响正在评估时,isPending 将被设置为 true。这不会以任何方式阻塞 UI,因此其他组件在过渡执行期间仍然可以正常工作。
使用 Transition Hook
我们现在将使用 Transition Hook 来避免在显示大量评论时阻塞 UI。让我们开始吧:
-
通过执行以下命令将
Chapter07_2文件夹复制到一个新的Chapter07_3文件夹:$ cp -R Chapter07_2 Chapter07_3 -
在 VS Code 中打开新的
Chapter07_3文件夹。 -
编辑
src/components/comment/CommentSection.jsx并导入useTransition函数:import { useState, **useTransition** } from 'react' -
定义一个 Transition Hook,如下所示:
export function CommentSection() { const [showComments, setShowComments] = useState(false) **const** **[isPending, startTransition] =** **useTransition****()** -
在
handleClick函数中,开始一个过渡:function handleClick() { **startTransition****(****() =>** **{** setShowComments((prev) => !prev) **})** }
转换有特定的用途和限制。例如,不要使用转换来处理受控输入状态,因为转换是非阻塞的,但我们实际上希望输入状态立即更新。此外,在转换内部,所有更新都需要立即调用。虽然通常可以在转换中等待异步函数,但在转换内部等待异步请求完成以更新状态是不可能的。如果您需要在更新状态之前等待异步请求,最好在处理函数中await它,然后启动转换。有关更多信息,请参阅 React 文档上的故障排除指南:react.dev/reference/react/useTransition#troubleshooting
-
我们现在可以在转换挂起时禁用按钮:
<button onClick={handleClick} **disabled****=****{isPending}**>
测试非阻塞转换
现在我们可以测试评论部分,看看它是否不再阻塞 UI。按照以下步骤操作:
-
按照以下步骤运行项目:
$ npm run dev -
通过访问
http://localhost:5173/在浏览器中打开前端。 -
点击一个显示评论按钮。
-
您会发现按下按钮后,UI 的其余部分仍然保持响应。尝试按下其他显示评论按钮之一——现在它工作了,并触发了另一个转换!
如我们所见,通过使用转换,我们可以在造成渲染计算密集型组件的状态更新时保持 UI 的响应性!
示例代码
本节的示例代码可以在Chapter07/Chapter07_3文件夹中找到。请检查文件夹内的README.md文件,以获取设置和运行示例的说明。
使用乐观钩子实现乐观更新
处理更新/变异有两种方式:
-
显示加载状态并在加载期间禁用某些操作
-
进行乐观更新,这立即在客户端显示了操作的成果,而变异仍在进行中。然后,在变异完成后,从服务器状态更新本地状态。
根据您的使用场景,一个或另一个选项可能更适合。通常,乐观更新非常适合快节奏的操作,例如聊天应用。而如果没有乐观更新的加载状态,则更适合关键操作,例如银行转账。
乐观钩子的签名如下:
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn)
如我们所见,它接受一个state(通常这是一个服务器状态)和一个用于处理更新的updateFn函数。然后,它返回一个optimisticState和一个addOptimistic函数,可以用来乐观地添加一个新项目到状态中。
updateFn接受两个参数,即传递给addOptimistic函数的currentState和optimisticValue。然后,它返回一个新的乐观状态。
实现乐观评论创建
在我们的案例中,我们将实现一种使用乐观更新创建新评论的方法。让我们开始做这件事:
-
通过执行以下命令将
Chapter07_3文件夹复制到新的Chapter07_4文件夹:$ cp -R Chapter07_3 Chapter07_4 -
在 VS Code 中打开新的
Chapter07_4文件夹。 -
创建一个新的
src/components/comment/CreateComment.jsx文件并导入useContext函数和UserContext:import { useContext } from 'react' import { UserContext } from '@/contexts/UserContext.js' -
定义
CreateComment组件,它接受一个addComment函数:export function CreateComment({ addComment }) { -
从上下文中获取当前登录用户的
username:const [username] = useContext(UserContext) -
定义一个
submitAction,它调用addComment函数:async function submitAction(formData) { const content = formData.get('content') const comment = { author: username, content, } await addComment(comment) } -
定义一个
<form>并将其submitAction传递给它:return ( <form action={submitAction}> <input type='text' name='content' /> <i> ~ {username}</i> <input type='submit' value='Create' /> </form> ) }
如我们所见,也可以在不使用 Action 状态钩子的情况下定义表单操作。然而,那样我们只能得到一个简单的处理表单提交的函数,而没有任何表单状态处理功能(例如挂起和错误状态)。
-
编辑
src/components/comment/CommentList.jsx并导入以下内容:import { useContext, useState, useOptimistic } from 'react' import { UserContext } from '@/contexts/UserContext.js' import { CreateComment } from './CreateComment.jsx' -
删除 以下代码:
const comments = Array.from({ length: 1000 }, (_, i) => ({ id: i, content: `Comment #${i}`, author: 'test', })) -
然后,定义一个上下文钩子来获取当前登录用户的
username:const [username] = useContext(UserContext) -
接下来,定义一个状态钩子来存储评论:
const [comments, setComments] = useState([])
为了使本节简短并切中要点,我们只关注乐观更新,你可以在这里自行实现将评论存储在数据库中的方法。
-
现在,定义乐观钩子:
const [optimisticComments, addOptimisticComment] = useOptimistic( comments, -
在更新函数中,我们将评论添加到数组中,并将
sending属性设置为true。我们稍后会使用这个属性来在视觉上区分乐观创建的评论和真实评论:(state, comment) => [ ...state, { ...comment, sending: true, id: Date.now(), }, ], )
我们在这里还定义了一个乐观评论的临时 ID,我们稍后可以用它来作为 key 属性。
-
现在,定义
addComment函数,它首先乐观地添加评论,然后等待一秒钟,然后将它添加到“数据库”中:async function addComment(comment) { addOptimisticComment(comment) await new Promise((resolve) => setTimeout(resolve, 1000)) setComments((prev) => [...prev, comment]) } -
按照以下方式渲染乐观评论:
return ( <div> {**optimisticComments**.map((comment) => ( <Comment {...comment} key={comment.id} /> ))} -
如果还没有评论,我们可以显示一个空状态:
{optimisticComments.length === 0 && <i>No comments</i>} -
如果用户已登录,我们允许他们创建新的评论:
**{username &&** **<****CreateComment****addComment****=****{addComment}** **/>****}** </div> ) } -
最后,编辑
src/components/comment/Comment.jsx并向其中添加sending属性:export function Comment({ content, author, **sending** }) { -
然后,从其中 删除 以下代码:
let startTime = performance.now() while (performance.now() - startTime < 1) { // do nothing for 1 ms } -
现在,根据发送属性更改颜色,以灰色显示乐观插入的评论:
return ( <div style={{ padding: '0.5em 0', **color:****sending** **? '****gray****'** **:** **'****black****' }}>** -
按照以下步骤运行项目:
$ npm run dev -
通过访问
http://localhost:5173/在浏览器中打开前端。 -
使用顶部的表单登录,然后按下一个 显示评论 按钮。它应该显示 没有评论 的消息。
-
输入一条新的评论并按 创建。
你会看到评论最初以灰色颜色乐观地插入,然后一秒钟后它将以黑色出现,表示评论已成功存储在“数据库”中:
图 7.1 – 乐观地插入新评论
示例代码
本节示例代码可在Chapter07/Chapter07_4文件夹中找到。请检查文件夹内的README.md文件,了解如何设置和运行示例。
如您所见,乐观 Hook 是实现乐观更新并保持应用程序快速响应的绝佳方式!
摘要
在本章中,我们首先学习了如何使用表单动作和动作状态 Hook 处理表单提交和状态。然后,我们模拟了处理渲染计算密集型组件时可能出现的潜在问题:阻塞 UI。接下来,我们通过引入转换 Hook 以非阻塞方式更改状态来解决此问题,允许 UI 在计算密集型组件渲染时保持响应。最后,我们学习了如何实现乐观更新,以便在等待异步操作完成的同时立即显示结果。
在下一章中,我们将学习如何使用 Hooks 在我们的博客应用程序中实现客户端路由。
问题
为了回顾本章学到的内容,尝试回答以下问题:
-
我们可以使用哪个特性来处理 React 19 中的表单提交?
-
在 React 19 中处理表单数据使用的网络标准是什么?
-
哪个 Hook 用于处理不同的表单状态?
-
在渲染计算密集型组件时可能出现的潜在问题是什么?
-
我们如何避免这个问题?
-
转换的限制是什么?
-
我们可以使用哪个 Hook 在状态完成持久化到服务器之前在客户端显示状态?
进一步阅读
如果你对本章学到的概念感兴趣,想了解更多信息,请查看以下链接:
-
FormDataAPI:developer.mozilla.org/en-US/docs/Web/API/FormData -
使用 React 进行表单提交:
react.dev/reference/react-dom/components/form -
动作状态 Hook:
react.dev/reference/react/useActionState -
关于乐观更新的更多信息:
dev.to/_jhohannes/why-your-applications-need-optimistic-updates-3h62
在 Discord 上了解更多
要加入本书的 Discord 社区——在那里你可以分享反馈,向作者提问,了解新版本——请扫描下面的二维码:
第八章:使用 Hooks 进行路由
在上一章中,我们学习了如何使用Action State Hook处理表单提交,如何使用Transition Hook避免 UI 阻塞,以及如何使用Optimistic Hook实现乐观更新。
在本章中,我们将学习如何在我们的博客应用中通过使用React Router实现客户端路由。首先,我们将学习 React Router 是如何工作的,以及它提供了哪些功能。然后,我们将创建一个新的路由来查看单个帖子,并使用Param Hook从 URL 中获取帖子 ID。接下来,我们将学习如何使用Link组件链接到不同的路由。最后,我们将学习如何使用Navigation Hook编程式实现导航以重定向到新创建的帖子。
本章将涵盖以下主题:
-
介绍 React Router
-
创建新路由并使用 Param Hook
-
使用 Link 组件链接到路由
-
使用 Navigation Hook 进行编程式重定向
技术要求
应该已经安装了相当新的 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/Chapter08
强烈建议您自己编写代码。不要简单地运行书中提供的代码示例。自己编写代码对于正确学习和理解代码非常重要。然而,如果您遇到任何问题,您始终可以参考代码示例。
介绍 React Router
React Router 最初是一个简单、声明式的路由库。它为我们提供了定义和管理应用程序不同路由的功能,以及在这些路由之间导航。最近,React Router 也可以用作 React 框架,提供处理布局和高级服务器端渲染的方法。然而,由于本书专注于 Hooks,我们将专注于将 React Router 作为库。
该库由三个主要组件组成:
-
BrowserRouter组件,它提供了一个上下文来使用路由 -
Routes组件,它允许我们定义一些路由并渲染当前活动路由的组件 -
Route组件,它允许我们定义一个特定的路由和要渲染的组件
此外,该库提供了创建指向特定路由的链接的组件(使用 Link 和 NavLink 组件),以及从 URL 获取参数(参数钩子)和导航(导航钩子)的钩子。
现在,让我们开始设置 React Router 和索引路由(它将包含我们博客的主页,显示博客帖子的源)。索引路由将是我们的服务器主 URL 上提供的内容,有时也称为入口点或 / 路由。
设置 React Router
按照以下步骤开始设置 React Router 库和索引路由:
-
通过执行以下命令将
Chapter07_4文件夹复制到新的Chapter08_1文件夹:$ cp -R Chapter07_4 Chapter08_1 -
在 VS Code 中打开新的
Chapter08_1文件夹。 -
打开一个终端,并按照以下方式安装
react-router库:$ npm install --save-exact react-router@7.2.0 -
创建一个新的
src/pages/文件夹,我们将把应用程序的各种页面放在其中。 -
创建一个新的
src/pages/Home.jsx文件来包含我们博客应用程序的主页(将显示我们之前已有的帖子源)。 -
在其中,导入
Suspense、PostFeed和ThemeContext:import { Suspense } from 'react' import { PostFeed } from '@/components/post/PostFeed.jsx' import { ThemeContext } from '@/contexts/ThemeContext.js' -
定义并导出
Home组件,该组件在加载帖子时显示回退,然后以特殊颜色显示特色帖子,然后显示常规帖子:export function Home() { return ( <Suspense fallback={<strong>Loading posts...</strong>}> <ThemeContext.Provider value={{ primaryColor: 'salmon' }}> <PostFeed featured /> </ThemeContext.Provider> <PostFeed /> </Suspense> ) } -
编辑
src/App.jsx并删除Suspense的导入,因为我们不再需要它了:import { useState**,** **Suspense** } from 'react' -
此外,删除
PostFeed组件的导入:import { PostFeed } from './components/post/PostFeed.jsx' -
然后,从
react-router中导入BrowserRouter、Routes和Route:import { BrowserRouter, Routes, Route } from 'react-router' -
此外,导入
Home页面组件:import { Home } from './pages/Home.jsx' -
在
App组件内部,定义BrowserRouter,确保它包装了所有组件,这样我们就可以在标题组件中使用导航钩子:export function App() { const [username, setUsername] = useState('') return ( <QueryClientProvider client={queryClient}> <UserContext.Provider value={[username, setUsername]}> <ThemeContext.Provider value={{ primaryColor: 'black' }}> **<****BrowserRouter****>** <div style={{ padding: 8 }}> <UserBar /> <br /> {username && <CreatePost />} <hr /> -
在
ErrorBoundary内部,替换Suspense组件及其所有子组件。相反,渲染Routes组件,在其中我们可以为我们的应用程序定义路由:<QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={FetchErrorNotice} > **<****Routes****>** -
定义一个索引路由,用于渲染
Home页面组件:**<****Route****index****element****=****{****<****Home** **/>****} />** **</****Routes****>** </ErrorBoundary> )} </QueryErrorResetBoundary> </div> **</****BrowserRouter****>** </ThemeContext.Provider> </UserContext.Provider> </QueryClientProvider> ) } -
按照以下方式运行应用程序:
$ npm run dev
当在浏览器中打开应用程序时,你会看到它看起来与之前完全一样,但现在主页是通过 React Router 而不是硬编码来渲染的!
示例代码
本节的示例代码可以在Chapter08/Chapter08_1文件夹中找到。请检查文件夹内的README.md文件,以获取设置和运行示例的说明。
创建一个新的路由并使用 Param Hook
现在我们已经成功设置了 React Router,我们可以开始创建一个新的路由来查看单个帖子。这个路由看起来如下所示:/post/:id,其中:id是一个包含要查看的帖子 ID 的 URL 参数。
URL 参数是在 URL 中使用的参数,用于定义动态内容。例如,在/post/:id路由中,/post/部分将是一个静态字符串,但:id将被替换为动态帖子 ID。假设你有一个以/post/8结尾的 URL,这意味着该路由与设置为8的id参数匹配。
让我们开始设置页面和路由:
-
通过执行以下命令将
Chapter08_1文件夹复制到一个新的Chapter08_2文件夹:$ cp -R Chapter08_1 Chapter08_2 -
在 VS Code 中打开新的
Chapter08_2文件夹。 -
编辑
src/api.js并定义一个新的函数来获取单个帖子:export async function fetchPost({ id }) { const res = await fetch(`/api/posts/${id}`) return await res.json() } -
编辑
src/components/post/Post.jsx并导入useSuspenseQuery和fetchPost函数:import { useSuspenseQuery } from '@tanstack/react-query' import { fetchPost } from '@/api.js' -
将
Post组件更改为仅接受id属性:export function Post({ **id** }) { -
在
Post组件内部,添加一个Suspense Query Hook来获取帖子并获取所有数据:const { data } = useSuspenseQuery({ queryKey: ['post', id], queryFn: async () => await fetchPost({ id }), }) const { title, content, author } = data -
创建一个新的
src/pages/ViewPost.jsx文件。在文件内部,导入Suspense,从react-router导入useParams函数和Post组件:import { Suspense } from 'react' import { useParams } from 'react-router' import { Post } from '@/components/post/Post.jsx' -
定义并导出
ViewPost页面组件:export function ViewPost() { -
使用 Params Hook 从 URL 参数中获取
id:const { id } = useParams() -
使用
Suspense边界在帖子获取时提供回退,然后渲染Post组件:return ( <Suspense fallback={<strong>Loading post...</strong>}> <Post id={id} /> </Suspense> ) } -
编辑
src/App.jsx并导入ViewPost组件:import { ViewPost } from './pages/ViewPost.jsx' -
然后,为
ViewPost页面定义一个新的带有:id参数的路由:<Routes> <Route index element={<Home />} /> **<****Route****path****=****'post/:id'****element****=****{****<****ViewPost** **/>****} />** </Routes> -
按照以下方式运行应用程序,在整个章节的其余部分保持运行状态:
$ npm run dev
现在可以通过在浏览器中的 URL 后附加/post/:id来手动访问单个帖子页面(例如/post/1):
图 8.1 – 在我们新定义的路由上查看单个帖子
然而,如果我们能通过点击主页上的主帖流中的某个帖子来访问这个页面,那就太好了。让我们在下一节中通过使用Link组件来实现这个功能。
使用组件链接到路由
当处理用户可以点击以访问不同页面的链接时,最好且最简单的方法是使用Link组件。这个组件将自动为我们创建一个指向特定页面的简单链接。
让我们开始使用Link组件来提供一个指向单个帖子的链接:
-
创建一个新的
src/components/post/PostListItem.jsx文件,在其中我们将定义Post组件的简化版本,该版本将在PostList组件中显示。在文件内部,导入useContext函数,ThemeContext和Link组件从react-router:import { useContext } from 'react' import { ThemeContext } from '@/contexts/ThemeContext.js' import { Link } from 'react-router' -
定义并导出
PostListItem组件,它接受帖子id、title和author作为 props:export function PostListItem({ id, title, author }) { -
定义一个 Context Hook 来获取主题:
const theme = useContext(ThemeContext) -
渲染标题,就像我们之前做的那样:
return ( <div> <h3 style={{ color: theme.primaryColor }}>{title}</h3> -
现在,渲染一个
Link组件,它将导航到/post/:id并显示ViewPost页面:<div> <Link to={`/post/${id}`}>View Post ></Link> </div> -
然后,显示作者,但不显示内容,以避免使信息过载:
<br /> <i> Written by <b>{author}</b> </i> </div> ) } -
编辑
src/components/post/PostList.jsx并将Post导入替换为PostListItem组件的导入:import { **PostListItem** } from './**PostListItem**.jsx' -
将
PostListItem组件渲染代替Post组件:{posts.map((post) => ( <Fragment key={post.id}> <**PostListItem** {...post} />
现在可以从首页跳转到单个帖子:
图 8.2 – 链接组件渲染“查看帖子 >”链接以跳转到单个帖子页面
但仍然没有返回首页的方法。让我们在下一节中实现它。
使用<NavLink>定义导航栏
如果我们想要给链接添加样式,例如,实现一个显示我们当前所在页面的导航栏,我们可以使用NavLink组件。
让我们使用这个组件来实现一个带有返回首页链接的导航栏:
-
创建一个新的
src/components/NavBarLink.jsx文件。在其内部,导入NavLink组件:import { NavLink } from 'react-router' -
定义并导出一个组件,它接受一个
toprop,用于定义我们应该链接到哪个路由,以及一个childrenprop 来提供要放在链接上的文本或组件:export function NavBarLink({ children, to }) { return ( <NavLink to={to} -
然后,定义一个
style,在其中检查链接是否处于激活状态(当我们当前在页面上时),然后以粗体形式渲染它:style={({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal', })} > {children} </NavLink> ) } -
编辑
src/App.jsx并导入NavBarLink组件,如下所示:import { NavBarLink } from './components/NavBarLink.jsx' -
在我们的博客应用的头部部分,在
UserBar之前定义一个返回索引/首页的NavBarLink:<BrowserRouter> <div style={{ padding: 8 }}> **<****NavBarLink****to****=****'/'****>****Home****</****NavBarLink****>** **<****hr** **/>** <UserBar />
现在我们有了一种从首页跳转到单个帖子,然后再回到首页查看其他帖子的方法:
图 8.3 – 渲染一个“首页”NavLink,当前处于激活状态(粗体)
接下来,让我们看看在创建新帖子后如何程序化地导航到单个帖子页面。
使用 Navigation Hook 进行程序化导航
每当我们想要程序化导航而不是让用户点击链接时,我们可以使用 React Router 提供的 Navigation Hook。Navigation Hook 提供了一个用于程序化导航的函数。
让我们现在开始使用 Navigation Hook:
-
编辑
src/components/post/CreatePost.jsx并导入useNavigate函数:import { useNavigate } from 'react-router' -
在
CreatePost组件内部定义一个 Navigate Hook:export function CreatePost() { const [username] = useContext(UserContext) **const** **navigate =** **useNavigate****()** -
在 Action State Hook 内部,从 mutation 中获取结果,然后重定向到新创建的帖子的
ViewPost页面:const [error, submitAction, isPending] = useActionState( async (currentState, formData) => { const title = formData.get('title') const content = formData.get('content') const newPost = { title, content, author: username, featured: false } try { **const** **result =** await createPostMutation.mutateAsync(newPost) **navigate****(****`/post/****${result.id}****`****)** } catch (err) { return err } }, ) -
尝试在博客应用中创建一个新的帖子,你会看到你被重定向到新创建的帖子页面!
我们已经在我们的博客应用程序中成功实现了路由!作为一个练习,你现在可以尝试在单独的页面上实现登录/注册表单和创建文章表单。在这样做的时候,我建议将主页链接重构为一个新的 NavBar 组件,其中包含链接到各个页面。
示例代码
本节示例代码位于 Chapter08/Chapter08_2 文件夹中。请检查文件夹内的 README.md 文件,以获取设置和运行示例的说明。
概述
在本章中,我们首先学习了 React Router 库的工作原理以及它由哪些组件组成。然后,我们设置了库以及博客主页的索引路由(显示博客文章的列表)。接下来,我们定义了一个新的路由来显示单独页面上的单个文章,并使用 Params 钩子从 URL 中获取 id 值。然后,我们学习了如何使用 Link 和 NavLink 组件导航到这个新路由以及如何返回主页。最后,我们学习了如何通过使用导航钩子程序化地导航到在文章成功创建后的路由。
在下一章中,我们将学习 React 提供的更高级的内置钩子。
问题
为了回顾本章学到的内容,尝试回答以下问题:
-
React Router 库由哪些组件组成?
-
我们如何使用 React Router 库定义一个新的路由?
-
我们如何在 URL 中读取动态值(参数)?
-
使用 React Router 定义链接有哪些方法,它们有何不同?
-
哪个钩子用于使用 React Router 程序化导航?
进一步阅读
如果你对本章学到的概念有更多兴趣,请查看以下链接:
- React Router 的官方网站:
reactrouter.com/
在 Discord 上了解更多
要加入本书的 Discord 社区——在那里你可以分享反馈、向作者提问以及了解新版本——请扫描下面的二维码:
packt.link/wnXT0
第九章:React 提供的高级 Hooks
在上一章中,我们学习了如何使用 React Router 实现路由。然后,我们学习了如何使用 Params Hook 实现动态路由。接下来,我们学习了如何使用 Link 组件提供不同路由的链接。最后,我们学习了如何使用 Navigation Hook 进行编程式重定向。
在本章中,我们将学习 React 提供的各种内置 Hooks。我们将首先概述内置 React Hooks,然后学习各种实用 Hooks。接下来,我们将学习如何使用 Hooks 来优化您应用程序的性能。最后,我们将学习关于高级 Effect Hooks 的内容。
到本章结束时,您将对 React 提供的所有内置 Hooks 有一个全面的了解。
本章将涵盖以下主题:
-
内置 React Hooks 概述
-
使用实用 Hooks
-
使用 Hooks 进行性能优化
-
使用 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/Chapter09。
强烈建议您亲自编写代码。不要简单地运行书中提供的代码示例。自己编写代码对于正确学习和理解代码非常重要。然而,如果您遇到任何问题,您始终可以参考代码示例。
内置 React Hooks 概述
React 提供了一些内置 Hooks。我们已经学习了 React 提供的基本 Hooks:
-
在第二章**,使用 State Hook中,使用
useState -
在第四章**,使用 Reducer 和 Effect Hooks中,使用
useEffect -
在第五章**,实现 React Contexts中,使用
useContext
此外,React 还提供了更多高级 Hooks,在某些用例中非常有用。我们已经介绍了以下高级 Hooks:
-
useReducer在 第四章*,使用 Reducer 和 Effect Hooks* 中。 -
useActionState在 第七章*,使用 Hooks 处理表单* 中。 -
useFormStatus(尚未介绍,但类似于useActionState) -
useOptimistic在 第七章*,使用 Hooks 处理表单* 中。 -
useTransition在 第七章*,使用 Hooks 处理表单* 中。
然而,React 还提供了更多的高级 Hooks:
-
useRef -
useImperativeHandle -
useId -
useSyncExternalStore -
useDebugValue -
useDeferredValue -
useMemo -
useCallback -
useLayoutEffect -
useInsertionEffect
首先,让我们回顾和总结一下我们已经学过的 Hooks。然后,我们将简要介绍 React 提供的所有这些高级 Hooks,并学习为什么以及如何使用它们。
useState
状态 Hook 返回一个值,该值将在重新渲染之间持续存在,以及一个用于更新它的函数。可以将 initialState 的值作为参数传递给它:
const [state, setState] = useState(initialState)
调用 setState 更新值并使用更新后的值重新渲染组件。如果值没有变化,React 不会重新渲染组件。
可以将一个函数传递给 setState 函数,第一个参数是当前值。例如,考虑以下代码:
setState(val => val + 1)
此外,如果初始状态是复杂计算的结果,可以将一个函数传递给 Hook 的第一个参数。在这种情况下,该函数将在 Hook 初始化期间只调用一次:
const [state, setState] = useState(() => {
return computeInitialState()
})
状态 Hook 是 React 提供的最普遍的 Hook。
我们在 第二章*,使用状态 Hook* 中使用了这个 Hook。
useEffect
Effect Hook 接受一个包含具有副作用(如计时器和订阅)的代码的函数。传递给 Hook 的函数将在渲染完成后、组件在屏幕上时运行:
useEffect(() => {
// do something
})
Hook 可以返回一个清理函数,当组件卸载时将被调用,例如,用于清理计时器或订阅:
useEffect(() => {
const interval = setInterval(() => {}, 100)
return () => {
clearInterval(interval)
}
})
如果组件在 effect 再次激活之前多次渲染,清理函数也将被调用。
为了避免在每次重新渲染时触发 effect,我们可以将值数组作为 Hook 的第二个参数。当这些值中的任何一个发生变化时,effect 将再次被触发:
useEffect(() => {
// do something when state changes
}, [state])
作为第二个参数传递的数组称为 effect 的依赖数组。如果你想使 effect 只在挂载时触发,并在卸载时清理,你可以将一个空数组作为第二个参数传递。
我们在 第四章*,使用 Reducer 和 Effect Hooks* 中使用了这个 Hook。
useContext
上下文钩子接受一个上下文对象,并返回上下文的当前值。当上下文提供者更新其值时,钩子将触发重新渲染,并带有最新的值:
const value = useContext(NameOfTheContext)
我们在 第五章*,实现 React 上下文* 中使用了这个钩子。
useReducer
Reducer 钩子是 useState 钩子的高级版本。它接受一个 reducer 作为第一个参数,它是一个具有两个参数的函数:state 和 action。然后,reducer 函数返回从当前状态和动作计算出的更新后的状态。如果 reducer 返回与上一个状态相同的值,React 不会重新渲染组件或触发效果:
const [state, dispatch] = useReducer(reducer, initialState, initFn)
在处理复杂状态变化时,我们应该使用 useReducer 钩子而不是 useState 钩子。处理全局状态也更容易,因为我们只需简单地传递 dispatch 函数而不是多个设置函数。
dispatch 函数是稳定的,在重新渲染时不会改变,因此可以从 useEffect 或 useCallback 依赖数组中省略它。
我们可以通过设置 initialState 值或指定一个 initFn 函数作为第三个参数来指定初始状态。当计算初始状态需要很长时间或我们想要通过动作重置状态时,指定此类函数是有意义的。
我们在 第四章*,使用 Reducer 和 Effect 钩子* 中使用了这个钩子。
useActionState
动作状态钩子的定义如下:
const [state, action, isPending] = useActionState(actionFn, initialState)
要定义一个动作状态钩子,我们需要提供一个 action 函数作为第一个参数,它具有以下签名:
function actionFn(currentState, formData) {
然后我们需要将 action 属性传递给一个 <form> 元素。当这个表单被提交时,动作函数会使用钩子的当前状态和表单内提交的 FormData 被调用。
此外,还可以为钩子提供一个 initialState,并使用 isPending 值在动作处理期间显示加载状态。
我们在 第七章*,使用钩子处理表单* 中使用了这个钩子。
useFormStatus
表单状态钩子的定义如下:
const { pending, data, method, action } = useFormStatus()
它用于我们未处理表单提交的情况。例如,如果我们有一个后端为我们处理表单提交,或者如果我们正在使用服务器操作来处理表单状态(在执行全栈 React 开发时相关)。
它返回一个具有以下属性的 status 对象:
-
pending:如果父<form>正在提交,则设置为true -
data:包含父表单提交的FormData -
method:设置为'get'或'post',取决于父<form>中定义了哪种方法。 -
action:如果向父<form>传递了动作函数,这将包含对该函数的引用。否则,它将是null。
例如,它可以用来实现一个在表单提交到服务器端时禁用的提交按钮:
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return <button disabled={pending}>Submit</button>
}
function ExampleForm() {
return (
<form>
<SubmitButton />
</form>
)
}
表单状态钩子只能用于在 <form> 内部渲染的组件中。与其他钩子不同,在撰写本文时,这是唯一从 react-dom 而不是 react 导出的钩子。
useOptimistic
乐观钩子具有以下签名:
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn)
它可以在我们等待从服务器获取远程状态更新完成时乐观地更新状态。它接受一个状态(通常来自 API 请求,例如一个查询钩子)和一个 update 函数。然后钩子返回一个乐观状态和一个添加乐观状态的功能。
例如,乐观钩子可以在我们等待服务器完成添加操作时将新对象插入数组中。在这种情况下,更新函数看起来如下:
function updateFn(state, newObject) {
return state.concat(
{ ...newObject, pending: true }
)
}
这个更新函数乐观地插入一个新对象,但给它添加一个 pending: true 标志,这样我们就可以稍后以不同的方式渲染挂起对象(例如,稍微变灰)。
我们在第七章**,使用钩子处理表单中使用了这个钩子。
useTransition
过渡钩子允许你通过更新状态而不阻塞用户界面来处理异步操作。这对于渲染计算密集型的组件树特别有用,例如渲染标签及其(可能复杂的)内容,或者当制作客户端路由器时。过渡钩子具有以下签名:
const [isPending, startTransition] = useTransition()
可以使用 isPending 状态来处理加载状态。startTransition 函数允许我们传递一个函数来启动过渡。这个函数需要是同步的。当函数内部触发更新(例如,设置状态)并评估其对组件的影响时,isPending 将被设置为 true。
这不会阻塞用户界面,因此在过渡执行期间,其他组件仍然可以正常工作。
我们在第七章**,使用钩子处理表单中使用了这个钩子。
在回顾我们已经学过的内置钩子之后,现在让我们继续学习其他高级内置钩子,这些钩子我们尚未使用,从 React 提供的内置实用钩子开始。
使用实用钩子
我们首先学习关于实用钩子的内容。这些钩子允许我们模拟某些用例或在我们开发自己的钩子时帮助我们,如在第十二章**,构建自己的钩子中所述。
我们现在将在我们的博客应用中设置一个演示页面,以便能够测试各种实用钩子。
让我们开始设置演示页面来测试这些钩子:
-
通过执行以下命令将
Chapter08_2文件夹复制到新的Chapter09_1文件夹:$ cp -R Chapter08_2 Chapter09_1 -
在 VS Code 中打开新的
Chapter09_1文件夹。 -
创建一个新的
src/components/demo/文件夹。这是我们稍后放置演示组件以尝试我们将要学习的各种 Hooks 的地方。 -
创建一个新的
src/pages/Demo.jsx文件,内容如下:export function Demo() { return <h1>Demo Page</h1> } -
编辑
src/App.jsx并导入Demo页面:import { Demo } from './pages/Demo.jsx' -
然后,为它定义一个新的
NavBarLink:<BrowserRouter> <div style={{ padding: 8 }}> <NavBarLink to='/'>Home</NavBarLink> **{' | '}** **<****NavBarLink****to****=****'/demo'****>****Demo****</****NavBarLink****>** -
最后,为它定义一个路由:
<Routes> <Route index element={<Home />} /> <Route path='post/:id' element={<ViewPost />} /> **<****Route****path****=****'demo'****element****=****{****<****Demo** **/>****} />** -
启动
dev服务器并在整章中保持其运行,如下所示:$ npm run dev -
点击导航栏中的Demo链接以打开演示页面。
现在我们有一个演示页面,我们可以开始学习 React 提供的其他内置高级 Hooks 了!
图 9.1 – 我们博客应用中的 Demo 页面
useRef
Ref Hook返回一个ref对象,可以通过ref属性将其分配给组件或元素:
const refContainer = useRef(initialValue)
在将 ref 对象分配给元素或组件后,可以通过refContainer.current访问 ref 对象。如果设置了initialValue,则在分配之前refContainer.current将被设置为这个值。
ref 对象可用于各种用例,但主要有两个:
-
获取一个元素的引用以在文档对象模型(DOM)中访问它
-
保持可变值,这些值不应受 React 生命周期的影响(例如,当值被突变时不会触发重新渲染)
使用 Ref Hook 自动聚焦输入字段
我们可以使用 Ref Hook 获取输入字段元素的引用,然后通过 DOM 访问其focus()函数来实现渲染时自动聚焦的输入字段。虽然也可以通过 HTML 为元素提供autofocus属性,但有时需要程序化地完成它——例如,如果我们想在用户完成其他操作后聚焦一个字段。
让我们现在开始使用 Ref Hook 实现自动聚焦输入字段的实现:
-
创建一个新的
src/components/demo/useRef/文件夹。 -
创建一个新的
src/components/demo/useRef/AutoFocus.jsx文件。在其内部,导入useRef和useEffect:import { useRef, useEffect } from 'react' -
然后,定义组件和一个 Ref Hook:
export function AutoFocus() { const inputRef = useRef(null) -
接下来,定义一个在渲染时被调用并导致输入字段聚焦的 Effect Hook:
useEffect(() => inputRef.current.focus(), []) -
渲染输入字段并将 Ref 传递给它:
return ( <div> <h3>AutoFocus</h3> <input ref={inputRef} type='text' /> </div> ) } -
现在,编辑
src/pages/Demo.jsx并导入AutoFocus组件:import { AutoFocus } from '@/components/demo/useRef/AutoFocus.jsx' -
通过调整组件如下,在
Demo页面上渲染它:export function Demo() { **return** **(** **<****div****>** **<****h1****>****Demo Page****</****h1****>** **<****h2****>****useRef****</****h2****>** **<****AutoFocus** **/>** **</****div****>** **)** }
刷新页面;你应该看到输入字段正在自动聚焦。
图 9.2 – 输入字段正在自动聚焦
在一个 ref 中更改状态
重要的是要注意,修改 ref 的当前值不会导致重新渲染。如果需要这样做,我们可以使用一个 ref 回调函数。这个函数将在元素加载时被调用。例如,我们可以使用这个函数来获取 DOM 中元素的初始大小。然后,我们可以在回调函数内部设置 State Hook 的状态来触发重新渲染。
如果我们不仅想要获取组件的初始宽度,还想要获取当前宽度(即使组件后来被调整大小),我们需要使用 布局 Effect Hook。我们将在本章的 使用 Hooks 进行高级效果 部分稍后介绍这个用例。
现在我们尝试在 refs 中使用回调函数来获取组件的初始宽度:
-
创建一个新的
src/components/demo/useRef/InitialWidthMeasure.jsx文件。在其中,导入useState函数:import { useState } from 'react' -
然后,定义组件和一个 State Hook 来存储组件的宽度:
export function InitialWidthMeasure() { const [width, setWidth] = useState(0) -
现在,定义一个用于
ref的回调函数,该函数接受 DOMnode作为参数:function measureRef(node) { -
检查我们是否成功获取了 DOM 节点的引用,然后使用 DOM API 获取元素的当前宽度:
if (node !== null) { setWidth(node.getBoundingClientRect().width) } } -
渲染组件并通过
ref属性添加回调函数:return ( <div> <h3>InitialWidthMeasure</h3> <div ref={measureRef}>I was initially {Math.round(width)}px wide</div> </div> ) } -
编辑
src/pages/Demo.jsx并在那里导入InitialWidthMeasure组件:import { InitialWidthMeasure } from '@/components/demo/useRef/InitialWidthMeasure.jsx' -
最后,在 Demo 页面上渲染组件:
export function Demo() { return ( <div> <h1>Demo Page</h1> <h2>useRef</h2> <AutoFocus /> **<****InitialWidthMeasure** **/>**
现在,Demo 页面应该会在您的浏览器中自动刷新并显示组件及其初始宽度!
图 9.3 – 显示组件初始宽度的组件
使用 refs 在重新渲染之间持久化可变值
Refs 可以用来访问 DOM,但也可以在组件重新渲染时保持可变值,例如存储间隔的引用。
让我们通过实现一个计算经过秒数的计时器来尝试一下:
-
创建一个新的
src/components/demo/useRef/Timer.jsx文件。在其中,导入useRef、useState和useEffect函数:import { useRef, useState, useEffect } from 'react' -
然后,定义并导出
Timer组件:export function Timer() { -
在其中,定义一个用于存储间隔的 Ref Hook 和一个用于存储当前计数的 State Hook:
const intervalRef = useRef(null) const [seconds, setSeconds] = useState(0) -
定义一个将增加计数的函数:
function increaseSeconds() { setSeconds((prevSeconds) => prevSeconds + 1) } -
现在,定义一个 Effect Hook 来定义一个新的间隔并将其存储在 ref 中:
useEffect(() => { intervalRef.current = setInterval(increaseSeconds, 1000) -
我们现在可以使用这个 ref 在组件卸载时清除间隔:
return () => clearInterval(intervalRef.current) }, []) -
渲染计时器的当前计数:
return ( <div> <h3>Timer</h3> {seconds} seconds -
最后,渲染一个按钮来取消计时器:
<button type='button' onClick={() => clearInterval(intervalRef.current)}> Cancel </button> </div> ) }
如果我们不需要在 Effect Hook 之外访问间隔 ID,我们可以在 effect 中简单地使用一个 const 而不是定义一个 Ref。虽然我们可以使用 State Hook 来存储间隔 ID,但这会导致组件重新渲染。正如我们所见,Refs 对于存储需要改变但又不用于渲染的值是理想的。
-
编辑
src/pages/Demo.jsx并在那里导入Timer组件:import { Timer } from '@/components/demo/useRef/Timer.jsx' -
最后,在 Demo 页面上渲染组件:
export function Demo() { return ( <div> <h1>Demo Page</h1> <h2>useRef</h2> <AutoFocus /> <InitialWidthMeasure /> **<****Timer** **/>**
现在 Demo 页面应该会在你的浏览器中自动刷新并显示计数秒数的组件!按下取消按钮停止计时器。
使用 refs 的方式与前面的例子相似,这使得它们类似于类中的实例变量,例如this.intervalRef。
以下截图显示了在页面打开后 42 秒,Demo 页面上的Timer组件的外观:
图 9.4 – 显示自打开页面以来经过的秒数的计时器组件
将引用作为属性传递
有时候,你可能想要获取另一个组件内部输入字段的引用(例如,当处理自定义输入字段时)。在过去,这需要forwardRef辅助函数。然而,自从 React 19 以来,我们可以简单地通过属性传递 refs。
让我们试试看:
-
创建一个新的
src/components/demo/useRef/CustomInput.jsx文件。 -
在其中定义以下自定义输入组件,接受一个引用作为属性:
export function CustomInput({ ref }) { -
我们现在可以像往常一样使用 refs:
return <input ref={ref} type='text' /> } -
现在,编辑
src/components/demo/useRef/AutoFocus.jsx文件并导入CustomInput组件:import { CustomInput } from './CustomInput.jsx' -
将输入字段替换为我们的
CustomInput组件,并将引用传递给它:return ( <div> <h3>AutoFocus</h3> **<****CustomInput****ref****=****{inputRef}** **/>**
刷新 Demo 页面,你会看到输入字段仍然在自动聚焦!
只创建一次 refs 内容
如果你有一个需要初始化的复杂算法,例如路径查找算法,你可以将其存储在 refs 中,以避免在每次渲染时创建它。这应该这样做:
function Map() {
const pathfinderRef = useRef(null)
if (pathfinderRef.current === null) {
pathfinderRef.current = createPathfinder()
}
}
通常,在渲染中像那样写入或读取ref.current在 React 中是不允许的。然而,在这种情况下,这是可以的,因为条件使得它只在一开始初始化组件时执行一次。
虽然 React 总是只保存 refs 的初始值一次,但直接在 Ref Hook 内部调用函数,如useRef(createPathfinder()),会在每次渲染时无谓地执行昂贵的函数。
正如我们所见,refs 有很多用例。通常,refs 对于以下操作很有用:
-
在重新渲染之间存储信息,因为——与常规变量不同——refs 在重新渲染时不会重置
-
在不触发重新渲染的情况下更改信息,因为——与 State Hooks 不同——refs 不会触发重新渲染
-
存储每个组件副本本地的信息,因为——与组件外部的常规变量不同——refs 在组件的不同实例之间没有共享值
useImperativeHandle
强制处理 Hook可以用来自定义当将ref指向它时暴露给其他组件的实例值。然而,应该尽可能避免这样做,因为它紧密耦合了组件,这会损害可重用性。
useImperativeHandle函数具有以下签名:
useImperativeHandle(ref, createHandle, [dependencies])
我们可以使用这个 Hook,例如,来公开一个特殊的 focus 函数,该函数不仅聚焦输入字段,还突出显示它。然后,其他组件可以通过对组件的 ref 调用此函数。现在让我们试试看:
-
创建一个新的
src/components/demo/useImperativeHandle/文件夹。 -
在其中,创建一个新的
src/components/demo/useImperativeHandle/HighlightFocusInput.jsx文件。 -
导入
useImperativeHandle、useRef和useState函数:import { useImperativeHandle, useRef, useState } from 'react' -
然后,定义一个接受
ref的组件:export function HighlightFocusInput({ ref }) { -
在组件内部,我们为输入字段定义一个 Ref Hook 和一个用于存储
highlight状态的状态 Hook:const inputRef = useRef(null) const [highlight, setHighlight] = useState(false) -
现在,定义一个 Imperative Handle Hook,将其
ref传递给它,并传递一个返回对象的函数:useImperativeHandle(ref, () => ({ -
此对象包含一个
focus函数,它将在input元素上触发focus函数,并将highlight状态设置为true一秒钟:focus: () => { inputRef.current.focus() setHighlight(true) setTimeout(() => setHighlight(false), 1000) }, })) -
最后,渲染一个输入字段并将
inputRef传递给它:return ( <input ref={inputRef} type='text' -
如果
highlight设置为true,则以yellow颜色渲染背景:style={{ backgroundColor: highlight ? 'yellow' : undefined }} /> ) } -
创建一个新的
src/components/demo/useImperativeHandle/HighlightFocus.jsx文件。 -
在其中,导入
useRef函数和HighlightFocusInput组件:import { useRef } from 'react' import { HighlightFocusInput } from './HighlightFocusInput.jsx' -
现在,定义一个组件和一个 Ref Hook:
export function HighlightFocus() { const inputRef = useRef(null) -
渲染一个按钮以触发组件中的
focus函数,然后渲染组件并将ref传递给它:return ( <div> <h3>HighlightFocus</h3> <button onClick={() => inputRef.current.focus()}>focus it</button> <HighlightFocusInput ref={inputRef} /> </div> ) } -
编辑
src/pages/Demo.jsx并导入HighlightFocus组件:import { HighlightFocus } from '@/components/demo/useImperativeHandle/HighlightFocus.jsx' -
渲染
HighlightFocus组件,如下所示:<InitialWidthMeasure /> <Timer /> **<****h2****>****useImperativeHandle****</****h2****>** **<****HighlightFocus** **/>** </div> ) }
现在,转到 Demo 页面并点击 focus it 按钮。你会看到输入字段被聚焦并高亮显示!
图 9.5 – 组件聚焦并突出显示输入字段
如我们所见,通过使用 refs 和 Imperative Handle Hook,我们可以访问其他组件中的函数。然而,这应该谨慎使用,因为它紧密耦合了组件,当我们的应用增长并且我们想在其他地方重用组件时,可能会成为一个问题。
useId
Id Hook 用于生成唯一的 ID。这可以很有用,例如,为元素提供 IDs 以提供无障碍属性(如 aria-labelledby 或 aria-describedby)。Id Hook 具有以下签名:
const uniqueId = useId()
让我们通过为复选框字段提供一个标签来尝试它:
-
创建一个新的
src/components/demo/useId/文件夹。 -
在其中,创建一个新的
src/components/demo/useId/AriaInput.jsx文件。 -
导入
useId函数:import { useId } from 'react' -
然后,定义一个使用 Id Hook 为标签生成 ID 的组件:
export function AriaInput() { const inputId = useId() -
渲染一个带有
htmlFor标签指向inputId的标签:return ( <div> <h3>AriaInput</h3> <label htmlFor={inputId}> -
使用生成的 ID 渲染一个
checkbox字段:<input id={inputId} type='checkbox' /> I agree to the Terms and Conditions. </label> </div> ) } -
编辑
src/pages/Demo.jsx并导入AriaInput组件:import { AriaInput } from '@/components/demo/useId/AriaInput.jsx' -
然后,在 Demo 页面上渲染该组件:
<Timer /> <h2>useImperativeHandle</h2> <HighlightFocus /> **<****h2****>****useId****</****h2****>** **<****AriaInput** **/>** </div> ) }
现在,转到 Demo 页面并打开输入字段的检查器;你会看到 React 为我们生成了一个 :r0: ID:
图 9.6 – React 自动生成的唯一 ID
在 React 19.1 中,ID 的格式从 :r123: 更改为 <<r123>>,以确保它们是有效的 CSS 选择器。
除了将标签连接到屏幕阅读器的输入字段(提高可访问性)外,使用 <label> 元素还有一个额外的优势,即允许我们点击标签来检查/取消选中复选框。
尽管在这种情况下我们可以手动设置一个 ID,例如,tos-check,但 ID 需要在整个页面上是唯一的。因此,如果我们想再次渲染相同的输入字段,ID 已经无效,因为它被重用了。为了防止这个问题,在这些情况下始终优先使用 Id Hook,以便组件可以在同一页面上多次重用。如果你在单个组件中有多个输入字段,最佳实践是在组件中只使用一个 Id Hook,然后通过添加标签到生成的 ID 来扩展 ID – 例如:`${id}-tos-check` 和 `${id}-username`。
useSyncExternalStore
Sync External Store Hook 用于订阅外部存储,例如状态管理库或浏览器 API。它的签名如下:
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
如我们所见,Sync External Store Hook 接受三个参数,并返回存储的当前快照,可以用来渲染其中的信息。参数如下:
-
第一个参数,
subscribe,是一个函数,它接受一个callback函数作为参数并将其订阅到存储中。当存储发生变化时,提供的函数应该被调用。subscribe函数还应该返回一个函数来清理订阅。 -
第二个参数,
getSnapshot,是一个函数,它返回存储中当前数据状态的快照。如果存储发生变化(subscribe函数中的callback函数被调用),React 会调用getSnapshot函数并检查返回的值是否不同。如果是,组件将重新渲染。 -
第三个参数,
getServerSnapshot,是一个可选的函数,它返回存储中当前数据状态的初始快照。这个函数仅在服务器渲染期间被调用,并用于在客户端恢复服务器渲染的内容。
在大多数情况下,使用 State 和 Reducer Hooks 而不是这个 Hook 会更好。大多数状态管理库也提供了它们自己的 Hooks。这个 Hook 主要在集成现有的非 React 代码时有用,但在与某些浏览器 API 交互时也有用,这正是我们现在要尝试的。
让我们通过使用 Sync External Store Hook 订阅浏览器 API 来实现一个检查网络连接是否可用的指示器:
-
创建一个新的
src/components/demo/useSyncExternalStore/文件夹。 -
在其中,创建一个新的
src/components/demo/useSyncExternalStore/OnlineIndicator.jsx文件。 -
导入
useSyncExternalStore函数:import { useSyncExternalStore } from 'react' -
定义一个接受
callback函数作为参数的subscribe函数:function subscribe(callback) { -
添加来自浏览器的
online和offline事件的监听器:window.addEventListener('online', callback) window.addEventListener('offline', callback) -
返回一个将清理那些事件监听器的函数:
return () => { window.removeEventListener('online', callback) window.removeEventListener('offline', callback) } } -
现在,定义一个
getSnapshot函数,该函数返回当前的在线状态:function getSnapshot() { return navigator.onLine } -
然后,定义组件和一个同步外部存储 Hook:
export function OnlineIndicator() { const isOnline = useSyncExternalStore(subscribe, getSnapshot) -
根据浏览器 API 的结果定义状态:
const status = isOnline ? 'online' : 'offline' -
渲染状态:
return ( <div> <h3>OnlineIndicator</h3> {status} </div> ) } -
编辑
src/pages/Demo.jsx并导入OnlineIndicator组件:import { OnlineIndicator } from '@/components/demo/useSyncExternalStore/OnlineIndicator.jsx' -
在 Demo 页面上渲染组件:
<h2>useId</h2> <AriaInput /> **<****h2****>****useSyncExternalStore****</****h2****>** **<****OnlineIndicator** **/>** </div> ) }
现在,转到 Demo 页面,如果你在线的话,它应该显示 在线。关闭所有网络连接以查看它变为 离线。
图 9.7 – 通过外部存储(浏览器 API)检测用户已离线
useDebugValue
调试值 Hook 对于开发作为共享库一部分的自定义 Hook 非常有用。它可以用于在 React DevTools 中显示某些值以进行调试。其签名如下:
useDebugValue(value, format)
第一个参数 value 是应该记录的值或消息。第二个可选的 format 参数用于提供一个格式化函数,该函数将在显示之前格式化值。
让我们通过定义一个用于 OnlineIndicator 组件的自定义 Hook 简单尝试一下:
-
编辑
src/components/demo/useSyncExternalStore/OnlineIndicator.jsx并导入useDebugValue函数:import { useSyncExternalStore**, useDebugValue** } from 'react' -
然后,在定义组件之前定义一个新的 Hook 函数:
function useOnlineStatus() { const isOnline = useSyncExternalStore(subscribe, getSnapshot) const status = isOnline ? 'online' : 'offline' -
添加调试值 Hook,如下所示:
useDebugValue(status) -
从 Hook 返回状态:
return status } -
调整组件以使用自定义 Hook:
export function OnlineIndicator() { **const** **status =** **useOnlineStatus****()** return ( <div> <h3>OnlineIndicator</h3> {status} </div> ) }
现在,转到 Demo 页面。如果你还没有安装 React 开发者工具 扩展,请为你的浏览器安装它(遵循 react.dev/learn/react-developer-tools 上的说明)。转到浏览器检查器的 Components 选项卡并选择 OnlineIndicator 组件。
你将看到自定义 Hook 的调试值在那里显示:
图 9.8 – 在 React 开发者工具中显示我们自定义 Hook 的状态
在了解了 React 提供的各种内置实用 Hook 之后,让我们继续学习如何使用内置 Hooks 进行性能优化。
使用 Hooks 进行性能优化
某些钩子可以用来优化你应用程序的性能。一般来说,一个经验法则是不要过早优化。这在 React 19 中引入的 React 编译器中尤其如此。如今,React 编译器自动为我们优化了大多数情况。所以,请记住,只有在你已经确定了应用程序中特定的性能问题时才使用这些钩子。一般来说,一个经验法则是除非你知道它将是一个昂贵的计算,否则不要过早优化。
React 编译器是一个可以手动安装的 Babel 插件,它也包含在某些框架中,如 Next.js。有关 React 编译器的更多信息,请阅读 React 文档中的以下页面:react.dev/learn/react-compiler。
useDeferredValue
延迟值钩子可以用来延迟低优先级的更新(例如过滤列表),以便先处理高优先级的更新(例如更新输入字段中输入的文本)。
例如,如果你有一个可以输入文本以过滤项目的搜索,延迟值钩子可以用来延迟过滤的更新。与设置固定时间后更新持久化的防抖不同,延迟是动态的,并且依赖于 UI 渲染的速度。在较快的机器上,它将更频繁地更新,而在较慢的机器上,更新不会减慢 UI 的其他部分。
useDeferredValue函数的签名如下所示:
const deferredValue = useDeferredValue(value, initialValue)
第一个参数是要延迟的值。例如,这个值可以来自处理用户输入的状态钩子。
第二个参数是一个可选的初始值,用于组件的初始渲染。如果没有定义初始值,钩子将在初始渲染期间不进行延迟,因为没有值可以渲染,直到value被设置(例如,用户在输入字段中键入)。
实现无延迟值的搜索
让我们先实现一个搜索页面,其中包含博客文章的搜索,不使用延迟值:
-
编辑
src/api.js并定义一个函数来产生人工延迟,以便我们可以模拟搜索操作缓慢:function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } -
接下来,定义一个
searchPostsAPI 函数,该函数获取所有帖子(特色和非特色):export async function searchPosts(query) { const res = await fetch('/api/posts') const posts = await res.json() -
然后,使用简单的搜索过滤帖子(将标题和查询转换为小写,然后检查查询是否包含在标题中):
const filteredPosts = posts.filter((post) => { const title = post.title.toLowerCase() return title.includes(query.toLowerCase()) }) -
添加一个一秒的延迟:
await sleep(1000) -
然后,返回过滤后的帖子:
return filteredPosts } -
创建一个新的
src/components/post/PostSearchResults.jsx文件。在其内部,导入以下内容:import { useSuspenseQuery } from '@tanstack/react-query' import { searchPosts } from '@/api.js' import { PostList } from './PostList.jsx' -
现在,定义一个组件,该组件将使用 Suspense 查询钩子、我们的 API 函数和
PostList组件来显示给定查询的搜索结果:export function PostSearchResults({ query }) { const { data } = useSuspenseQuery({ queryKey: ['posts', query], queryFn: () => searchPosts(query), }) return <PostList posts={data} /> } -
接下来,创建一个新的
src/components/post/PostSearch.jsx文件。在其内部,导入以下内容:import { useState, Suspense } from 'react' import { PostSearchResults } from './PostSearchResults.jsx' -
定义一个
PostSearch组件,该组件使用状态钩子和输入字段来处理查询:export function PostSearch() { const [query, setQuery] = useState('') return ( <div> <input value={query} onChange={(e) =>setQuery(e.target.value)} /> -
定义一个
Suspense边界,并在其中渲染PostSearchResults组件:<Suspense fallback={<h4>loading...</h4>}> <PostSearchResults query={query} /> </Suspense> </div> ) } -
创建一个新的
src/pages/Search.jsx文件。在其内部,导入PostSearch组件:import { PostSearch } from '@/components/post/PostSearch.jsx' -
按如下方式渲染包含
PostSearch组件的页面:export function Search() { return ( <div> <h1>Search posts</h1> <PostSearch /> </div> ) } -
编辑
src/App.jsx并导入Search页面:import { Search } from './pages/Search.jsx' -
添加如下链接到页面:
<BrowserRouter> <div style={{ padding: 8 }}> <NavBarLink to='/'>Home</NavBarLink> **{' | '}** **<****NavBarLink****to****=****'/search'****>****Search****</****NavBarLink****>** {' | '} <NavBarLink to='/demo'>Demo</NavBarLink> -
最后,定义路由:
<Routes> <Route index element={<Home />} /> <Route path='post/:id' element={<ViewPost />} /> <Route path='demo' element={<Demo />} /> **<****Route****path****=****'search'****element****=****{****<****Search** **/>****} />** </Routes>
现在前往 搜索 页面并输入一个查询;你会看到在显示新结果之前,会显示 加载中… 一秒钟。
图 9.9 – 等待新结果加载
虽然这个搜索功能正常工作,但在用户输入查询时用 加载中… 消息替换所有结果并不是一个很好的用户体验。
引入延迟值
使用延迟值钩子,我们可以在新结果正在获取时显示旧查询结果,一旦它们准备好,就无缝地替换它们。
现在让我们开始使用延迟值钩子:
-
编辑
src/components/post/PostSearch.jsx并导入useDeferredValue函数:import { useState, Suspense**, useDeferredValue** } from 'react' -
在
PostSearch组件内部定义延迟值钩子:export function PostSearch() { const [query, setQuery] = useState('') **const** **deferredQuery =** **useDeferredValue****(query)** -
现在,将
SearchResults组件的query替换为deferredQuery:<Suspense fallback={<h4>loading...</h4>}> <SearchResults query={**deferredQuery**} /> </Suspense> </div> ) }
前往 搜索 页面并在搜索输入字段中输入查询;你会看到在新的结果到来之前,会显示之前的结果。现在 加载中… 消息仅在首次输入查询之前显示!
图 9.10 – 显示新结果正在加载时的过时结果
useMemo
Memo 钩子会捕获一个函数的结果并将其缓存。这意味着它不会每次都重新计算。这个钩子可以用于性能优化:
const memoizedVal = useMemo(
() => computeVal(a, b, c),
[a, b, c]
)
在上一个示例中,computeVal 是一个性能密集型函数,它从 a、b 和 c 计算出一个结果。
useMemo 在渲染期间运行,所以请确保计算函数不会引起任何副作用,例如资源请求。副作用应该放入 useEffect 钩子中。
作为第二个参数传递的数组指定了函数的依赖项。如果这些值中的任何一个发生变化,函数将被重新计算;否则,将使用存储的结果。如果没有提供数组,则每次渲染都会计算一个新的值。如果传递了一个空数组,则值只计算一次。
不要仅依赖 useMemo 来只计算一次。如果这些之前缓存的值长时间未被使用,例如为了释放内存,React 可能会忘记它们。仅将其用于性能优化。
自 React 19 以来,React 编译器尝试尽可能自动地记忆化值。在大多数情况下,不再需要手动使用useMemo包装值。只有当你发现 React 编译器没有以令人满意的方式记忆化性能问题时,才使用这个钩子。作为一个经验法则,尽量不要过早地优化你的组件,除非你有非常充分的理由这样做。
useCallback
useCallback钩子与useMemo钩子的工作方式类似。然而,它返回一个记忆化的回调函数而不是一个值:
const memoizedCallback = useCallback(
() => doSomething(a, b, c),
[a, b, c]
)
之前的代码类似于以下useMemo钩子:
const memoizedCallback = useMemo(
() => () => doSomething(a, b, c),
[a, b, c]
)
返回的函数只有在依赖数组中传递的值之一发生变化时才会重新定义。
与记忆钩子类似,回调钩子只有在确定了 React 编译器没有以令人满意的方式处理的特定性能问题时才应使用,例如无限重渲染循环或渲染次数过多。
现在我们已经了解了如何使用内置钩子进行性能优化,让我们简要介绍一下 React 提供的最后几个内置钩子:效果钩子的高级版本。
使用钩子进行高级效果
效果钩子有两个特殊版本:布局效果钩子和插入效果钩子。这些钩子仅适用于非常特定的用例,所以我们在这里只简要介绍它们。
useLayoutEffect
布局效果钩子与效果钩子相同,但它会在所有 DOM 突变完成后、组件在浏览器中渲染之前同步触发。它可以用来在渲染前从 DOM 中读取信息并调整组件的外观。在这个钩子内部进行的更新将在浏览器渲染组件之前同步处理。
除非真的需要,否则不要使用这个钩子,这通常只出现在某些边缘情况下。useLayoutEffect会阻塞浏览器中的视觉更新,因此比useEffect慢。
这里的规则是首先使用useEffect。如果你的突变改变了 DOM 节点的外观,这可能导致它闪烁,你应该使用useLayoutEffect。
useInsertionEffect
插入效果钩子与效果钩子类似,但它会在任何布局效果触发之前触发。这个钩子仅适用于 CSS-in-JS 库的作者,所以你很可能不需要它。
示例代码
本章的示例代码可以在Chapter09/Chapter09_1文件夹中找到。请检查文件夹内的README.md文件,以获取设置和运行示例的说明。
总结
在这一章中,我们学习了 React 19.1 版本提供的所有钩子。我们首先概述了内置钩子,然后学习了各种实用钩子。接下来,我们转向学习用于性能优化的钩子。最后,我们学习了高级效果钩子。
我们现在对所有不同的内置 Hooks 有了概述。
在下一章,我们将学习关于使用 React 社区开发的各类 Hooks,以及如何找到更多这样的 Hooks。
问题
为了回顾本章所学内容,尝试回答以下问题:
-
Ref Hook 的不同用例有哪些?
-
Imperative Handle Hook 增加了哪些功能?
-
我们应该在什么时候使用 Id Hook?
-
Sync External Store Hook 覆盖了哪些用例?
-
我们如何使用 Debug Value Hook?
-
使用 Deferred Value Hook 给我们带来了哪些优势?
-
我们应该在什么时候使用 Memo 和 Callback Hooks?
-
在大多数情况下,是否仍然有必要使用 Memo 和 Callback Hooks?
进一步阅读
如果你对本章所学概念有更多信息的兴趣,请查看以下链接:
-
React 文档中的内置 React Hooks 部分:
react.dev/reference/react/hooks -
关于 React 编译器的更多信息:
react.dev/learn/react-compiler
在 Discord 上了解更多
要加入这本书的 Discord 社区——在那里你可以分享反馈,向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/wnXT0