React 核心概念(六)
原文:
zh.annas-archive.org/md5/751c075f5f8c25e4f99f8fc75bd4f4d2译者:飞龙
第十七章:理解 React Suspense 与 use() 钩子
学习目标
到本章结束时,你将能够做到以下内容:
-
描述 React 的 Suspense 功能的目的和功能
-
使用 RSCs 与 Suspense 一起显示细粒度的回退内容
-
使用 React 的
use()钩子为客户端组件提供 Suspense -
为数据获取和回退内容应用不同的 Suspense 策略
简介
在 第十章 ,React 和优化机会背后的场景 中,在 通过代码拆分(懒加载)减少包大小 部分中,你学习了 React 的 <Suspense> 组件及其如何在懒加载和代码拆分的上下文中使用,以在下载代码包时显示回退内容。
如该处所述,Suspense 组件的目的是简化显示回退内容的过程,这反过来可以提高用户体验。由于大多数用户都不喜欢盯着过时的内容或空白页面,因此拥有一个显示替代内容的内置功能非常方便。
在本章中,你将了解到 React 的 Suspense 组件不仅限于用于代码拆分。相反,它还可以用于数据获取,在数据加载时显示一些临时内容(例如,从数据库中)。然而,正如你也将学到的,Suspense 只能在以特定方式获取数据时用于数据获取。
此外,本章将重新探讨在 第十一章 ,处理复杂状态 中引入的 use() 钩子。正如你将学到的,除了用于获取上下文值之外,这个钩子还可以与 Suspense 一起使用。
使用 Suspense 显示细粒度的回退内容
当获取数据或下载资源(例如,代码文件)时,可能会出现加载延迟——这些延迟可能导致糟糕的用户体验。因此,你应该考虑在等待请求的资源时显示一些临时的回退内容。
因此,为了简化在等待某些资源时渲染回退内容的过程,React 提供了其 Suspense 组件。如 第十章 ,React 和优化机会背后的场景 所示,你可以将 Suspense 组件用作围绕 React 元素的包装器,这些元素会获取一些代码或数据。例如,当在代码拆分的情况下使用它时,你可以显示一些临时的回退内容,如下所示:
import { lazy, **Suspense**, useState } from 'react';
const DateCalculator = lazy(() => import(
'./components/DateCalculator.jsx'
)
);
function App() {
const [showDateCalc, setShowDateCalc] = useState(false);
function handleOpenDateCalc() {
setShowDateCalc(true);
}
return (
<>
<p>This app might be doing all kinds of things.</p>
<p>
But you can also open a calculator which calculates
the difference between two dates.
</p>
<button onClick={handleOpenDateCalc}>Open Calculator</button>
**<****Suspense****fallback****=****{****<****p****>****Loading...****</****p****>****}>**
{showDateCalc && <DateCalculator />}
**</****Suspense****>**
</>
);
}
在这个例子(它来自一个基于 Vite 的常规 React 项目)中,React 的 Suspense 组件被包裹在条件渲染的 DateCalculator 组件周围。DateCalculator 是通过 React 的 lazy() 函数创建的,该函数用于按需(即按需)加载属于此组件的代码包。
因此,整个其他页面的内容从一开始就全部显示出来。在获取代码的过程中,只有条件性显示的DateCalculator组件被替换为回退内容(<p>Loading...</p>),而其他内容保持不变。因此,Suspense在非常细粒度级别上渲染一些回退 JSX 代码。与用临时内容替换整个页面或组件标记不同,这里只替换了 UI 的一小部分。
当然,Suspense因此提供了一种在获取数据时也很希望拥有的功能——毕竟,延迟在那里也经常发生。
使用 Next.js 进行数据获取的 Suspend
如前一章中所述,在使用 Next.js 管理加载状态部分,数据获取的过程也常常伴随着等待时间,这可能会对用户体验产生负面影响。这就是为什么,在同一部分中,你学习了 Next.js 允许你定义一个loading.js文件,该文件包含一些在延迟期间渲染的回退组件。
然而,使用这种方法实际上是用加载回退组件内容替换了整个页面(或该页面的主要区域)。但这并不总是理想的——你可能在获取数据时更希望在一个更细粒度级别上显示一些加载回退内容。
幸运的是,在 Next.js 项目中,你可以像前一个示例中那样使用Suspense,将其包裹在获取数据的组件周围。由于 Next.js 支持 HTTP 响应流,它能够在数据可用时立即渲染页面的其余部分,并将依赖于获取数据的内 容流式传输到客户端。在数据加载并可用之前,Suspense将渲染其定义的回退内容。
因此,回到第十六章React 服务器组件与服务器操作中使用 Next.js 管理加载状态的例子,你可以通过将数据获取代码外包给一个单独的UserGoals组件来利用Suspense:
import fs from 'node:fs/promises';
async function fetchGoals() {
await new Promise((resolve) => setTimeout(resolve, 3000)); // delay
const goals = await fs.readFile('./data/user-goals.json', 'utf-8');
return JSON.parse(goals);
}
export default async function UserGoals() {
const fetchedGoals = await fetchGoals();
return (
<ul>
{fetchedGoals.map((goal) => (
<li key={goal}>{goal}</li>
))}
</ul>
);
}
然后,可以在GoalsPage组件中将UserGoals组件包裹在Suspense中,如下所示:
import { Suspense } from 'react';
import UserGoals from '../../components/UserGoals';
export default async function GoalsPage() {
return (
<>
<h1>Top User Goals</h1>
<Suspense fallback={
<p id="fallback">Fetching user goals...</p>}
>
<UserGoals />
</Suspense>
</>
);
}
此代码现在利用 React 的Suspense组件在UserGoals组件获取数据时显示回退段落。
注意
你可以在 GitHub 上找到完整的演示项目代码:github.com/mschwarzmueller/book-react-key-concepts-e2/tree/17-suspense-use/examples/02-data-fetching-suspense。
因此,当用户导航到/goals时,他们立即看到标题(<h1>元素)与回退内容的组合。不再需要单独的loading.js文件。
图 17.1:回退内容作为目标页面的一部分显示,而不是完全替换它
然而,在这种情况下使用 Suspense 的优势不仅仅是 loading.js 文件不再需要。相反,现在可以非常细致地管理数据获取和回退内容。
例如,在一个更复杂的在线商店应用程序中,你可能有一个这样的组件:
function ShopOverviewPage() {
return (
<>
<header>
<h1>Find your next deal!</h1>
<MainNavigation />
</header>
<main>
<Suspense fallback={<DailyDealSkeleton />}>
<DailyDeal />
</Suspense>
<section id="search">
<h2>Looking for something specific?</h2>
<Search />
</section>
<Suspense fallback={<p>Fetching products...</p>}>
<Products />
</Suspense>
</main>
</>
);
}
在这个例子中,<header> 和 <section id="search"> 元素始终可见并渲染。另一方面,<DailyDeal /> 和 <Products /> 只在它们的数据被获取后渲染。在此之前,将显示各自的回退内容。
图 17.2:最初显示占位符,直到加载的数据流进并渲染到屏幕上
<DailyDeal /> 和 <Products /> 将独立于彼此加载和渲染,因为它们被两个不同的 Suspense 块包裹。因此,用户将立即看到页眉和搜索区域,然后最终看到每日特价和产品——尽管这两个中的任何一个都可能先加载和渲染。
这些示例中重要的是,被 Suspense 包裹的组件是使用 async/await 的 RSCs。正如你将在下一节中学习的,并非所有 React 组件都会与 Suspense 组件交互。但在 Next.js 项目中,React Server Components 会。
在其他 React 项目中使用 Suspense—可能,但棘手
上一节探讨了如何在 Next.js 项目中使用 Suspense 来利用 RSCs 进行数据获取。
然而,Suspense 不是一个 Next.js 特有的功能或概念——相反,它是 React 本身提供的。因此,你可以在任何 React 项目中使用它来在数据获取时显示回退内容。
至少,这是理论上的。但实际情况是,你无法与所有组件和数据获取策略一起使用它。
Suspense 与 useEffect() 不兼容
由于通过 useEffect() 获取数据是一种常见策略,你可能会倾向于将 Suspense 与此 Hook 结合使用,在数据通过效果函数加载时显示一些回退内容。
例如,以下 BlogPosts 组件使用 useEffect() 来加载和显示一些博客文章:
import { useEffect, useState } from 'react';
function BlogPosts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
async function fetchBlogPosts() {
// simulate slow network
await new Promise((resolve) => setTimeout(resolve, 3000));
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts'
);
const posts = await response.json();
setPosts(posts);
}
fetchBlogPosts();
}, []);
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
你可以将这个组件用 Suspense 包裹,如下所示:
import { Suspense } from 'react';
import BlogPosts from './components/BlogPosts.jsx';
function App() {
return (
<>
<h1>All posts</h1>
<Suspense fallback={<p>Fetching blog posts...</p>}>
<BlogPosts />
</Suspense>
</>
);
}
但不幸的是,这不会按预期工作。在数据获取时,不会渲染任何内容,而是显示回退内容。
这种行为的原因是 Suspense 的目的是在组件渲染过程中获取数据时挂起——而不是在某个效果函数内部获取数据时。
这有助于回忆 useEffect() 的工作原理(来自 第八章 ,处理副作用):效果函数在组件函数执行之后执行,即,在第一个组件渲染周期完成后。
因此,在通过 useEffect() 获取数据时,你不能使用 Suspense 来显示回退内容。相反,在这些情况下,你需要手动管理并使用执行数据获取的组件中的某些加载状态(即通过手动管理不同的状态片段,如 isLoading ——例如,如在第十一章 处理复杂状态 中所述,在 useState() 的局限性 和 使用 useReducer() 管理状态 部分中展示)。
在渲染过程中获取数据——错误的方式
由于 Suspense 的目的是在组件在渲染过程中获取数据时显示回退内容,你可以尝试重新编写 BlogPosts 组件,使其看起来像这样:
async function BlogPosts() {
await new Promise((resolve) => setTimeout(resolve, 3000));
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts'
);
const posts = await response.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
但尝试使用此代码将在浏览器开发者工具中产生错误:
图 17.3:React 在客户端对异步组件发出警告
React 不支持在客户端组件中使用 async/await。只有 React Server Components 可以使用该语法(因此返回承诺)。因此,未设置以支持 RSCs 的常规 React 项目无法使用此解决方案。
当然,你可以想出一个(有问题的)替代方案,如下所示:
function BlogPosts() {
const [posts, setPosts] = useState([]);
new Promise(() => setTimeout(() => {
return fetch(
'https://jsonplaceholder.typicode.com/posts'
).then(response => response.json())
.then(fetchedPosts => setPosts(fetchedPosts));
}, 3000));
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
但这种方法已经在第八章 处理副作用 中的 问题是什么? 部分中被弃用——该代码创建了一个无限循环。
所以,如果不与 RSCs(React Server Components)一起工作,将数据获取作为组件渲染过程的一部分是非常困难的。
获取 Suspense 支持颇具挑战性
由于 Suspense 需要在渲染过程中进行数据获取,这很难手动设置,因此 React 文档(react.dev/reference/react/Suspense#displaying-a-fallback-while-content-is-loading)本身提到:“只有启用 Suspense 的数据源才会激活 Suspense 组件”,进一步说明这些数据源包括:
-
使用像 Relay 和 Next.js 这样的
Suspense启用框架进行数据获取 -
使用
lazy()懒加载组件代码 -
使用
use()读取 Promise 的值
在同一页面上,官方文档强调:“尚未支持使用非意见化框架启用 Suspense 的数据获取。”
注意
文档可能会随着时间的推移而改变——React 也是如此。但即使在你阅读此内容时,确切的措辞可能有所不同,使用 Suspense 的方式以及它不能在没有特殊库或 lazy() 等功能的情况下使用的事实,极不可能改变。
这章是在 React 19 发布时编写的。你可以访问这本书的官方变更日志,以了解自那时以来是否有什么重大变化:github.com/mschwarzmueller/book-react-key-concepts-e2/blob/main/CHANGELOG.md。
因此,除非你打算构建自己的具有 Suspense 功能的库,否则你必须坚持使用 Suspense 进行代码拆分(通过 lazy()),使用与 Suspense 集成的第三方框架或库,或者探索 use() 钩子的使用。
当然,lazy() 函数(以及如何与 Suspense 一起使用)已经在 第十章 的 Behind the Scenes of React and Optimization Opportunities 部分的 Reducing Bundle Sizes via Code Splitting (Lazy Loading) 中进行了介绍。但其他两个选项——具有 Suspense 功能的库和 use() 钩子——又是如何的呢?
使用支持库进行数据获取的 Suspense
如你在 Using Suspense for Data Fetching with Next.js 部分所学,当使用 Next.js 时,你可以使用 Suspense 进行数据获取。但尽管 Next.js 是支持 Suspense 的最受欢迎的 React 框架之一,但它并不是你唯一的选择。
例如,TanStack Query(之前称为 React Query)是另一个流行的第三方库,它为数据获取解锁了 Suspense。这个库与 Next.js 不同,不是一个旨在帮助构建全栈 React 应用或运行服务器端代码的库。相反,TanStack Query 是一个专注于帮助客户端数据获取、数据变更和异步状态管理的库。由于它在客户端运行,因此它也适用于没有集成 SSR 和 RSC 的 React 项目——尽管你也可以在这样项目中使用它。
TanStack Query 是一个复杂且功能丰富的库——我们可能可以写一本书来专门介绍它。但以下简短的代码片段(来自一个基于 Vite 的项目,而不是 Next.js 项目)展示了如何借助该库获取数据:
import { useSuspenseQuery } from '@tanstack/react-query';
async function fetchPosts() {
await new Promise((resolve) => setTimeout(resolve, 3000));
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await response.json();
return posts;
}
function BlogPosts() {
const {data} = useSuspenseQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
在这个例子中,BlogPosts 组件使用 TanStack Query 的 useSuspenseQuery() 钩子,结合自定义的 fetchPosts() 函数,通过 HTTP 请求获取数据。正如钩子的名字所暗示的,它与 React 的 Suspense 组件集成。
因此,BlogPosts 组件可以像这样被 Suspense 包裹:
import { Suspense } from 'react';
import BlogPosts from './components/BlogPosts.jsx';
function App() {
return (
<>
<h1>All posts</h1>
<Suspense fallback={<p>Fetching blog posts...</p>}>
<BlogPosts />
</Suspense>
</>
);
}
正如你所知,Suspense 的使用方式与 lazy() 或 Next.js 中的使用方式相同。因此,其功能和使用方式没有改变——如果你正在将 Suspense 包裹在一个与 Suspense 集成的组件周围(例如 BlogPost 通过 TanStack Query 的 useSuspenseQuery() 钩子),则可以使用 Suspense 在数据获取过程进行时输出一些后备内容。
注意
你可以在 GitHub 上找到完整的示例项目:github.com/mschwarzmueller/book-react-key-concepts-e2/tree/17-suspense-use/examples/05-tanstack-query。
当然,这只是一个简单的例子。你可以用 TanStack Query 做更多的事情,还有其他可以与 Suspense 一起使用的库。重要的是要理解,除了 Next.js 之外,还有其他选择。但也要牢记,并非所有代码(以及并非所有库)都适用于 Suspense。
除了使用直接与 Suspense 集成的库(如通过 useSuspenseQuery() 钩子的 TanStack Query),你还可以借助 React 的内置 use() 钩子使用 Suspense 进行数据获取。
渲染时使用数据
React 提供的 use() 钩子不仅限于访问上下文值,如第十一章 处理复杂状态 中所示——相反,它还可以用来从承诺中读取值。
因此,你可以在组件的渲染过程中使用 use() 钩子来提取和使用承诺的值。use() 将自动与任何包装的 Suspense 组件交互,并让它了解数据获取过程的当前状态(即承诺是否已解决)。
因此,可以从 渲染时获取数据——错误方式 部分的示例调整为使用 use() 钩子,如下所示:
**import** **{ use }** **from****'react'****;**
async function fetchPosts() {
await new Promise((resolve) => setTimeout(resolve, 3000));
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts'
);
const posts = await response.json();
return posts;
}
function BlogPosts() {
**const** **posts =** **use****(****fetchPosts****());**
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
BlogPosts 组件现在不再是一个使用 async/await 的组件。相反,它使用导入的 use() 钩子来读取调用 fetchPosts() 生成的承诺的值。
如前所述,use() 与 Suspense 交互,因此 BlogPosts 可以像这样被 Suspense 包装:
import { Suspense } from 'react';
import BlogPosts from './components/BlogPosts.jsx';
function App() {
return (
<>
<h1>All posts</h1>
<Suspense fallback={<p>Fetching blog posts...</p>}>
<BlogPosts />
</Suspense>
</>
);
}
当运行此代码时,它可能按预期工作(取决于你使用的 React 版本),但更有可能不会产生任何结果,甚至在浏览器开发者工具中显示错误消息:
图 17.4:use() 钩子仅与由 Suspense 兼容库创建的承诺一起工作
如此错误消息所述,use() 钩子不打算与像上一个例子中创建的常规承诺一起使用。相反,它应该用于由 Suspense 兼容 库或框架提供的承诺。
注意
如果你想要违背官方建议并尝试构建支持 use() 和 Suspense 的承诺,你可以探索官方 React 文档中链接的官方 Suspense 示例项目(19.react.dev/reference/react/Suspense)——例如,这个项目:codesandbox.io/p/sandbox/strange-black-6j7nnj。
请注意,正如文档中提到的,该演示项目使用的是不稳定 API,可能无法与未来的 React 版本兼容。
因此,再次强调,需要第三方框架或库的支持。无论你尝试使用Suspense与在渲染过程中获取数据的组件(无论是否使用use())一起使用,你最终都需要帮助。
换句话说:为了利用Suspense,你需要直接通过一个与Suspense兼容的库或框架获取数据,或者你需要在一个由与Suspense兼容的库或框架生成的 promise 上使用use() Hook。
其中一个这样的框架又是 Next.js。除了在 RSC 周围使用Suspense(如使用 Next.js 进行数据获取的 suspense部分所示),你还可以将Suspense与 Next.js 生成的 promise 的use() Hook 结合使用。
使用 Next.js 创建的 promise 与 use()结合使用
Next.js 项目能够创建与use()和Suspense一起工作的 promise。更准确地说,你在 RSC 中创建并传递给(客户端)组件的任何 promise 都符合use()能用的 promise。
考虑以下示例代码:
import fs from 'node:fs/promises';
import UserGoals from '../../components/UserGoals';
async function fetchGoals() {
await new Promise((resolve) => setTimeout(resolve, 3000)); // delay
const goals = await fs.readFile('./data/user-goals.json', 'utf-8');
return JSON.parse(goals);
}
export default function GoalsPage() {
**const** **fetchGoalsPromise =** **fetchGoals****();**
return (
<>
<h1>Top User Goals</h1>
<UserGoals **promise****=****{fetchGoalsPromise}** />
</>
);
}
在这个代码片段中,通过调用fetchGoals()创建了一个 promise,并将其存储在一个名为fetchGoalsPromise的常量中。然后,创建的 promise(fetchGoalsPromise)被作为promise prop 的值传递给UserGoals组件。
此外,这个UserGoals组件与另一个组件一起定义在UserGoals.js文件中,如下所示:
import { use, Suspense } from 'react';
function Goals({ fetchGoalsPromise }) {
const goals = use(fetchGoalsPromise);
return (
<ul>
{goals.map((goal) => (
<li key={goal}>{goal}</li>
))}
</ul>
);
}
export default function UserGoals({ promise }) {
return (
<Suspense fallback={<p id="fallback">Fetching user goals...</p>}>
<Goals fetchGoalsPromise={promise} />
</Suspense>
);
}
在这个代码示例中,UserGoals组件使用Suspense包裹Goals组件,并将接收到的promise prop 值(通过fetchGoalsPromise prop)转发给该组件。然后,Goals组件通过use() Hook 读取该 promise 值。
由于 promise 是在由 Next.js 管理的 RSC(GoalsPage)中创建的,React 不会对此代码提出异议——Next.js 创建了与use()一起工作的 promise。相反,它会在数据获取时显示后备内容(<p id="fallback">Fetching user goals...</p>),一旦数据到达并被流式传输到客户端,就会渲染最终的用户界面。
如前所述,任何未被Suspense包裹的元素(例如,本例中的<h1>元素)将立即显示。
图 17.5:当通过use()获取数据时,后备文本显示在标题旁边
值得注意的是,UserGoals和Goals也都是 RSC(React Server Components),尽管如此,它们仍然可以使用use() Hook。
通常,Hooks 不能在 RSC 中使用,但use() Hook 是特殊的。正如它可以在if语句或循环中使用(如第十一章处理复杂状态中所述),它可以在服务器和客户端组件中执行。
然而,当与服务器组件一起工作时,你也可以简单地使用async/await而不是use()。因此,use()钩子实际上只有在客户端组件中读取 promise 值时才真正有用——在那里,async/await不可用。
在客户端组件中使用 use()
除了用于访问上下文之外,use()钩子被引入是为了帮助在客户端组件中读取 promise 值——即在你不能使用async/await的情况下。
考虑这个更新的用户目标示例,其中管理了一些状态并触发了一个副作用:
**'use client'****;**
import { use, Suspense, **useEffect,****useState** } from 'react';
// sendAnalytics() is a dummy function that just logs to the console
import { sendAnalytics } from '../lib/analytics';
function Goals({ fetchGoalsPromise }) {
**const** **[mainGoal, setMainGoal] =** **useState****();**
const goals = use(fetchGoalsPromise);
function handleSetMainGoal(goal) {
setMainGoal(goal);
}
return (
<ul>
{goals.map((goal) => (
<li
key={goal}
id={goal === mainGoal ? 'main-goal' : undefined}
onClick={() => handleSetMainGoal(goal)}
>
{goal}
</li>
))}
</ul>
);
}
export default function UserGoals({ promise }) {
**useEffect****(****() =>** **{**
**sendAnalytics****(****'user-goals-loaded'****, navigator.****userAgent****);**
**}, []);**
return (
<Suspense fallback={<p id="fallback">Fetching user goals...</p>}>
<Goals fetchGoalsPromise={promise} />
</Suspense>
);
}
在这个例子中,Goals组件使用useState()来管理用户标记为主要目标的目标信息。此外,UserGoals组件(使用Suspense)利用useEffect()钩子在一旦组件渲染时发送一个分析事件(即在挂起的Goals组件显示之前)。由于使用了所有这些客户端特有的功能,需要use client指令。
因此,async/await不能在Goals和UserGoals组件中使用。但由于use()钩子可以在客户端组件中使用,它为这种情况提供了一种可能的解决方案。而且,由于这个例子来自 Next.js 应用程序,React 不会对use()消耗的 promise 类型提出异议。相反,这段示例代码会导致在获取目标数据时显示后备内容。
Suspense 使用模式
如你所学,Suspense组件可以包裹在那些在渲染过程中获取数据的组件周围——只要它们以合规的方式进行。
当然,在许多项目中,你可能会有多个组件需要获取数据,并且在获取数据的同时应该显示一些后备内容。幸运的是,你可以根据需要频繁地使用Suspense组件——你甚至可以将多个Suspense组件相互组合。
一起揭示内容
到目前为止,在所有示例中,Suspense总是包裹在恰好一个组件周围。但没有任何规则阻止你将Suspense包裹在多个组件周围。
例如,以下代码是有效的:
function Shop() {
return (
<>
<h1>Welcome to our shop!</h1>
<Suspense fallback={<p>Fetching shop data...</p>}>
<DailyDeal />
<Products />
</Suspense>
</>
);
}
在这个代码片段中,DailyDeal和Products组件的数据获取是同时开始的。由于这两个组件都被一个单一的Suspense组件包裹,后备内容在两个组件完成数据获取之前会显示。所以,如果一个组件(例如DailyDeal)在一秒后完成,而另一个组件(Products)需要五秒,这两个组件只有在五秒后才会被揭示(并替换后备内容)。
图 17.6:数据并行获取,并通过 Suspense 显示后备内容,直到所有组件完成
尽快揭示内容
当然,有些情况下,您可能希望为多个组件显示后备内容,但又不希望等待所有组件完成数据获取后才显示任何获取的内容。
在这种情况下,您可以使用 Suspense 多次:
function Shop() {
return (
<>
<h1>Welcome to our shop!</h1>
<Suspense fallback={<p>Fetching daily deal data...</p>}>
<DailyDeal />
</Suspense>
<Suspense fallback={<p>Fetching products data...</p>}>
<Products />
</Suspense>
</>
);
}
在这个调整后的代码示例中,DailyDeal 和 Products 分别被两个不同的 Suspense 组件包裹。因此,每个组件的内容将在可用时被揭示,独立于其他组件的数据获取状态。
图 17.7:每个组件在完成数据获取后用最终内容替换其后备内容
嵌套挂起内容
除了并行获取数据外,您还可以使用嵌套的 Suspense 组件创建更复杂的加载顺序。
考虑这个例子:
function Shop() {
return (
<>
<h1>Welcome to our shop!</h1>
<Suspense fallback={<p>Fetching shop data...</p>}>
<DailyDeal />
<Suspense fallback={<p>Fetching products data...</p>}>
<Products />
</Suspense>
</Suspense>
</>
);
}
在这种情况下,最初,显示文本为“获取商店数据”的段落。幕后,DailyDeal 和 Products 组件中的数据获取开始。
一旦 DailyDeal 组件完成数据获取,其内容将被显示。同时,在 DailyDeal 下方,如果 Products 组件仍在获取数据,则渲染嵌套 Suspense 块的后备。
最后,一旦 Products 获取了其数据,内部 Suspense 组件的后备内容将被移除,并渲染 Products 组件。
图 17.8:嵌套的 Suspense 块导致顺序数据获取和内容揭示
因此,如您所见,您可以使用 Suspense 多次。此外,您可以将不同的 Suspense 组件组合起来,以便您可以创建所需的精确加载顺序和用户体验。
您应该通过 Suspense 还是 useEffect() 获取数据?
如您在本章中学习到的,您可以使用 Suspense 与 RSCs、Suspense 启用的库或 use() 钩子(这也需要支持库)一起获取数据,并在数据获取过程中显示一些后备内容。
或者,如第十一章处理复杂状态中所述,您也可以通过 useEffect() 和 useState() 或 useReducer() 手动获取数据并显示后备内容。在这种情况下,您实际上管理着决定是否在您的应用中显示某些加载后备内容的自身状态;使用 Suspense,React 会为您完成这项工作。
因此,选择哪种方法取决于你。使用 Suspense 可以节省相当多的代码,因为你不需要手动管理这些不同的状态片段。结合 Next.js 或 TanStack Query 这样的框架或库,数据获取可以比通过 useEffect() 手动进行时变得显著更容易。此外,Suspense 与 RSCs 和 SSR 集成,因此可以用于在服务器端获取数据——与 useEffect() 不同,它对服务器端没有影响(没有开玩笑)。
然而,如果你没有使用支持 Suspense 或 use() -启用承诺的库或框架,你除了回退到 useEffect()(因此不使用 Suspense 进行数据获取)之外,没有太多选择。随着未来 React 版本的更新,它们可能会提供帮助构建与 use() 一起工作的承诺的工具。但到目前为止,这基本上是在使用(正确的)库和 Suspense 或不使用库和 useEffect() 之间做出决定。
摘要和关键要点
-
Suspense组件可用于在数据获取或代码下载时显示回退内容。 -
对于数据获取,
Suspense仅与在渲染过程中通过Suspense启用的数据源获取数据的组件一起工作。 -
类似于 TanStack Query 和 Next.js 这样的库和框架支持使用
Suspense进行数据获取。 -
使用 Next.js,你可以将
Suspense包裹在使用async/await的服务器组件周围。 -
或者,
Suspense可以包裹在使用 React 的use()钩子读取承诺值的组件周围。 -
use()应仅用于读取以Suspense为目的解决的承诺的值——例如,由与Suspense兼容的第三方库创建的承诺。 -
当使用 Next.js 时,在 RSCs 中创建并通过 props 传递给(客户端)组件的承诺可以通过
use()消费。 -
use()钩子有助于在需要使用客户端特定功能(如useState())的组件中读取值和使用Suspense。 -
Suspense可以包裹任意数量的组件以同时获取数据和显示内容。 -
Suspense也可以嵌套以创建复杂的加载序列。
接下来是什么?
React 的 Suspense 功能非常有用,因为它有助于在代码或数据正在获取时精确地显示回退内容。同时,当涉及到数据获取时,使用 Suspense 可能很棘手,因为它仅与在渲染过程中通过 use() 钩子以正确方式获取数据的组件一起工作(例如,如果传递给钩子的承诺是 Suspense 兼容的)。
正因如此,本章还探讨了如何使用 Suspense 和 use() 与 Next.js 一起,以及该框架如何简化使用 Suspense 和 use() 获取数据和显示回退内容的过程。
尽管可能很复杂,但Suspense可以帮助创建出色的用户体验,因为它允许你在资源挂起时轻松显示后备内容。
本章还总结了作为 React 开发者你必须知道的 React 核心功能列表。当然,你总是可以深入研究,探索更多模式和第三方库。下一章(也是最后一章)将分享一些资源和可能的下一步行动,你可以在完成这本书后深入研究。
测试你的知识!
通过回答以下问题来测试你对本章涵盖的概念的了解。然后,你可以将你的答案与github.com/mschwarzmueller/book-react-key-concepts-e2/blob/17-suspense-use/exercises/questions-answers.md中可以找到的示例进行比较:
-
React 的
Suspense组件的目的是什么? -
组件需要如何获取数据才能与
Suspense一起工作? -
在使用 Next.js 时,
Suspense可以如何使用? -
use()钩子的目的是什么? -
use()钩子可以读取哪种类型的承诺? -
列出三种使用多个组件的
Suspense方法。
应用你所学的知识
在了解了关于 Next.js 的所有新知识之后,是时候将其应用到实际的演示项目中了。
在下一节中,你将找到一个活动,让你练习使用 Next.js 和Suspense。一如既往,你还需要应用前面章节中介绍的一些概念。
活动十七点一:在迷你博客中实现 Suspense
在这个活动中,你的任务是建立在活动 16.1完成的工程之上。在那里,建立了一个非常简单的博客。现在,你的任务是增强这个博客,以便在博客文章列表或单个博客文章的详细信息加载时显示一些后备内容。为了证明你的知识,你应该在起始页面(/)上通过async/await获取数据,并在blog/<some-id>页面上通过use()钩子获取数据。
此外,还应将可用的博客文章列表显示在单个博客文章的详细信息下方。当然,在获取该列表数据时,必须显示一些后备文本——尽管,该文本应独立于博客文章详细信息的后备内容显示。
注意
你可以在github.com/mschwarzmueller/book-react-key-concepts-e2/tree/17-suspense-use/activities/practice-1-start找到这个活动的起始项目快照。在下载此代码时,你将始终下载整个仓库。请确保然后导航到包含起始代码的子文件夹(在这种情况下是activities/practice-1-start),以使用正确的代码快照。
在提供的起始项目中,您将找到用于获取所有博客帖子和一个单个帖子的函数。这些函数包含人工延迟以模拟缓慢的服务器。
在项目文件夹中下载代码并运行 npm install 以安装所有必需的依赖项后,解决方案步骤如下:
-
将获取和显示博客帖子列表的逻辑外包到一个单独的组件中。
-
在起始页面上使用该组件,并使用 React 的
Suspense组件在获取博客帖子时显示一些合适的后备内容。 -
此外,将获取和渲染单个博客帖子详情的逻辑外包到一个单独的客户端 (!) 组件中。在
/blog/<some-id>页面上输出这个新创建的组件。 -
将获取博客详情的承诺传递给新创建的组件,并使用
use()钩子来读取其值。同时,利用Suspense组件输出一些后备内容。 -
重新使用获取并渲染博客帖子列表的组件,并在
/blog/<some-id>页面下方输出它。使用Suspense来显示一些后备内容,独立于博客帖子详情的数据获取状态。
最终页面应如以下截图所示:
图 17.9:在获取博客帖子时显示后备内容
图 17.10:在获取博客帖子详情和博客帖子列表时显示后备内容
注意
您可以在此处找到此活动的完整代码和示例解决方案:github.com/mschwarzmueller/book-react-key-concepts-e2/tree/17-suspense-use/activities/practice-1 .
第十八章:下一步和进一步资源
学习目标
到本章结束时,你将了解以下内容:
-
如何从阅读书籍到应用你的知识
-
如何最好地实践本书中学到的内容
-
你可以探索的下一个 React 主题
-
哪些流行的第三方 React 包可能值得仔细研究
简介
通过这本书,你已经全面(重新)了解了你必须知道的、为了成功使用 React 的关键 React 概念,为组件、属性、状态、上下文、React Hooks、路由、服务器端 React 以及许多其他关键概念提供了理论和实践指导。
但 React 不仅仅是一系列概念和想法的集合。它支持一个完整的第三方库生态系统,帮助解决许多常见的 React 特定问题。还有一个庞大的 React 社区,分享解决常见问题或流行模式的解决方案。
在这个最后的、简短的章节中,你将了解一些你可能想要探索的最重要和最受欢迎的第三方库。你还将被介绍到其他有助于学习 React 的优质资源。此外,本章还将分享一些关于如何在完成本书后作为 React 开发者继续前进和成长的建议。
你应该如何进行?
以本书中获得的知识为基础,进一步深入探索 Next.js,研究其他流行的 React 库,或者了解更多关于 React 替代品如 Angular 或 Vue 的内容。Web 开发提供了广泛的技术、语言、库、模式和概念。虽然这有时可能让人感到不知所措,但它也是一个巨大的机会池,可以帮助开发者成长并更好地解决复杂问题。
除了学习更多关于 React 和相关包的知识外,应用你的知识和实践你所学的也很重要。不要只是读一本书又一本书。相反,利用你新获得的知识来构建一些演示项目。
你不必构建下一个亚马逊或 TikTok。这些应用程序之所以由大型团队构建,是有原因的。但你应该构建一些关注几个核心问题的简单演示项目。例如,你可以构建一个非常基础的网站,让用户能够存储和查看他们的每日目标,或者构建一个基本的 Meetups 页面,让访客可以组织和参加聚会活动。
简单来说:实践是关键。你必须应用你所学的知识并构建一些东西。因为通过构建演示项目,你将自动遇到需要解决的问题,而你手头并没有解决方案。你必须尝试不同的方法,并在互联网上搜索可能的(部分)解决方案。最终,这就是你学习最多和培养解决问题的技能的方式。
你在这本书中找不到所有问题的解决方案,但这本书确实提供了基本的工具和构建块,这将帮助你解决这些问题。解决方案是通过组合这些构建块,并在本书中收集到的知识基础上构建而成的。
成为全栈 React 开发者
本书已经涵盖了开始基于 React 的后端开发所需的关键概念。第 15 章、16 章和 17 章探讨了服务器端渲染、Next.js、服务器组件和动作以及构建全栈 React 应用所需的相关功能。
因此,深入研究 Next.js 可能是下一步有趣的选择。借助官方文档或像我的 Next.js & React – The Complete Guide 在线课程,你可以获得成为全栈 React 开发者所需的知识。
而不仅仅局限于 Next.js:你还可以探索像 Remix 和 React Router(它正在获得更多的全栈功能)或 TanStack Start 这样的替代方案。如果你不介意没有像 Next.js 提供的完全集成的全栈开发体验,你还可以了解更多关于将解耦的后端连接到 React 前端的知识——即,你可以学习如何使用 Node.js 或任何其他后端语言构建和连接一个独立的后端(REST 或 GraphQL)API。
成为全栈开发者并不是你必须做的事情。这是一个选择,但根据你个人的偏好或你在团队中的角色,这可能不是最适合你的选择。重要的是要知道,使用 React 构建全栈应用是一条可能的探索路径——而且随着 Next.js 和类似框架的出现,这条路径变得相当容易。无论如何,如前所述,你应该通过构建演示项目来应用你的 React 知识和实践,无论你是否在深入研究全栈开发。
值得探索的有趣问题
那么,你可以探索和尝试构建哪些问题和演示应用?
通常,你可以尝试构建(简化版)流行的网络应用的克隆(例如亚马逊的高度简化版)。最终,你的想象力是无限的,但在接下来的几节中,你将找到三个项目想法的详细信息和相关挑战的建议。
构建购物车
一种非常常见的网站类型是在线商店。你可以找到各种产品的在线商店——从书籍、服装或家具等实体商品到视频游戏或电影等数字产品——构建这样的在线商店将是一个有趣的项目想法和挑战。
当然,在线商店确实拥有许多仅凭客户端 React 无法构建的功能。例如,整个支付过程主要是一个后端任务,其中请求必须由服务器处理。库存管理将是另一个在数据库和服务器上发生,而不是在网站访客的浏览器中发生的功能。因此,你可以使用 Next.js(或本章前面提到的替代方案之一)来处理这些后端功能,从而构建一个全栈 React 应用程序。但即使你不想深入全栈开发,在线商店也包含许多需要交互式用户界面的功能(因此,使用 React 的客户端功能会受益)。例如,你可以设置不同的页面来显示可用的产品列表、产品详情或订单的当前状态,正如你在第十三章,使用 React Router 的多页应用程序中学到的。你通常在网站上还有购物车。构建这样一个购物车,结合添加和删除项目的功能,同样会利用到多个 React 功能——例如,状态管理,如第四章,处理事件和状态中解释的那样。
所有这些都始于拥有几个用于虚拟产品(这些产品硬编码在前端代码中,而不是从某些后端获取)的页面(路由)、产品详情以及购物车本身。购物车显示需要通过应用程序全局状态(例如,通过上下文,如第十一章,处理复杂状态中所述)管理的项目,因为网站访客必须能够从产品详情页面添加项目到购物车。你还需要各种各样的 React 组件——其中许多必须是可重用的(例如,显示的单独购物车项目)。你对 React 组件和属性的了解,来自第二章,理解 React 组件和 JSX,以及第三章,组件和属性,将有助于此。
购物车状态也是一个非平凡的状态。一个简单的产品列表通常不足以解决问题——尽管你当然至少可以应用你在第五章,渲染列表和条件内容中学到的知识。相反,你必须检查一个项目是否已经是购物车的一部分,或者它是否是第一次添加。如果它已经是购物车的一部分,你必须更新购物车中项目的数量。当然,你还需要确保用户能够从购物车中删除项目或减少项目的数量。而且,如果你想更加复杂,你甚至可以模拟在更新购物车状态时必须考虑的价格变化。
如您所见,这个极其简单的虚拟在线商店已经提供了相当多的复杂性。当然,正如之前提到的,您也可以添加后端功能并将虚拟产品存储在数据库中。如果您愿意,可以更深入地学习 Next.js,构建一个基于 React 的更复杂的全栈应用程序。这使您能够应用在第十五章,服务器端渲染与构建 Next.js 全栈应用程序,以及第十六章,React 服务器组件与服务器操作中学到的知识。
构建应用程序的认证系统(用户注册和登录)
许多网站允许用户注册或登录。对于许多网站来说,在执行某些任务之前需要用户认证。例如,您必须创建一个 Google 账户,才能上传视频到 YouTube 或使用 Gmail(以及许多其他 Google 服务)。同样,在在线购买(数字)视频游戏或参加付费在线课程之前通常也需要一个账户。您不登录也无法进行在线银行操作。这只是简短的一览;还有更多例子可以添加,但您已经明白了。在许多网站上,出于各种原因都需要用户认证。
在更多网站上,这通常是可选的。例如,您可能可以以访客身份订购产品,但创建账户时您将获得额外的优势(例如,您可以跟踪您的订单历史或收集奖励积分)。
当然,构建自己的 YouTube 版本挑战性太强,不适合作为良好的实践项目。谷歌之所以雇佣了数千名开发者,是有原因的。然而,您可以识别并复制单个功能,例如用户认证。
使用 React 构建自己的用户认证系统。确保用户可以注册和登录。向您的网站添加一些示例页面(路由),并找到一种方法使某些页面仅对已登录用户可用。这些目标可能听起来不多,但实际上您在实现过程中会遇到很多挑战——这些挑战迫使您为全新的问题找到解决方案。
虽然您可以在 React 应用程序代码中使用一些虚拟(客户端)逻辑来模拟发送到您后台服务器的 HTTP 请求,但您也可以添加一个真实的演示后端。该后端需要在数据库中存储用户账户,验证登录请求,并发送回认证令牌,告知 React 前端用户的当前认证状态。在您的 React 应用程序中,这些 HTTP 请求将被视为副作用,如第八章,处理副作用中所述。
再次强调,如果你想要使用真正的后端,你还需要深入了解后端开发,要么构建一个单独的服务器端应用程序,要么使用 Next.js(或任何类似的完整栈 React 框架)。或者,你也可以使用 Firebase、Supabase、Auth0 或许多其他提供前端应用程序认证后端的服务。无论哪种方式,你都可以探索如何将你的 React 应用程序连接到这样的后端。
正如你所见,这个“简单”的项目想法(或者更确切地说,功能想法)提出了许多挑战,并将要求你基于 React 知识构建,并为广泛的问题找到解决方案。
构建一个活动管理网站
如果你首先尝试构建自己的购物车系统并开始用户认证,然后你可以更进一步,构建一个结合这些功能(并提供新的、额外的功能)的更复杂的网站。
其中一个项目想法就是一个活动管理网站。这是一个用户可以创建账户,一旦登录,就可以创建活动的网站。所有访客都可以浏览这些活动并注册。是否允许作为访客注册(而不先创建账户)取决于你。
你也可以选择是否要添加后端逻辑(即处理请求并将用户和活动存储在数据库中的服务器)或者你将简单地通过应用范围内的状态存储所有数据。如果不添加后端,所有数据将在页面重新加载时丢失,你将无法在其他机器上看到其他用户创建的活动,但你仍然可以练习所有这些关键的 React 功能。
对于这种类型的模拟网站,需要许多 React 功能:可重用组件、页面(路由)、组件特定的和全局状态、处理和验证用户输入、显示条件性和列表数据等等。
再次强调,这显然不是示例的完整列表。你可以构建任何你想要的东西。发挥创意,进行实验,因为只有当你用它来解决问题时,你才能真正掌握 React。
常见且流行的 React 库
无论你正在构建哪种类型的 React 应用程序,你都会在过程中遇到许多问题和挑战。从处理和验证用户输入到发送 HTTP 请求,复杂的应用程序伴随着许多挑战。
你可以自己解决所有挑战,甚至自己编写所有需要的(React)代码。而且,为了练习,这确实是一个好主意。但随着你构建越来越复杂的应用程序,外包某些问题可能是有意义的。
幸运的是,React 拥有丰富而充满活力的生态系统,提供了解决各种常见问题的第三方包。以下是一个简要的、非详尽的列表,列出了一些可能有所帮助的流行第三方库:
-
TanStack Query:一个在 React 应用程序中帮助数据获取、缓存和管理的非常流行的库(
tanstack.com/query/latest)。 -
Framer Motion:一个针对 React 的特定库,允许你在 React 应用程序中构建和实现强大且视觉上令人愉悦的动画(
www.framer.com/motion/)。 -
React Hook Form:一个简化处理和验证用户输入过程的库(
react-hook-form.com/)。 -
Formik:另一个流行的库,有助于处理和验证表单输入(
formik.org/)。 -
Axios:一个通用的 JavaScript 库,简化了发送 HTTP 请求和处理响应的过程(
axios-http.com/)。 -
Redux:在过去,这是一个必不可少的 React 库。如今,它仍然很重要,因为它可以极大地简化(复杂)跨组件或应用程序范围状态的管理(
redux.js.org/)。 -
Zustand:如果你需要额外的库来帮助管理 React 应用程序中的状态,你还可以探索 Zustand——这是 Redux 的一个非常受欢迎的替代品(
zustand-demo.pmnd.rs/)。
这只是一个关于一些有用和流行库的简短列表。由于潜在的挑战数不胜数,你还可以编制一个无限的库列表。在寻找解决其他问题的库时,搜索引擎和 Stack Overflow(一个开发者论坛)是你的好朋友。
使用 TypeScript
你也可以考虑在你的 React 项目中使用 TypeScript,而不是纯 JavaScript。
TypeScript 是一种 JavaScript 超集,它增加了强类型和严格类型。因此,使用 TypeScript 可以帮助你捕捉并避免与缺失值或错误值类型相关的某些错误。
你可以通过官方文档(react.dev/learn/typescript)或专门的在线课程或教程开始使用 TypeScript 进行 React 开发。
其他资源
正如之前提到的,React 确实拥有一个高度活跃的生态系统——这不仅仅体现在第三方库方面。你还会发现成千上万的博客文章,讨论各种最佳实践、模式、想法以及可能的解决方案。通过搜索正确的关键词(例如 使用 Hooks 的 React 表单验证),几乎总能找到有趣的文章或有用的库。
你还会找到大量的付费在线课程,例如 www.udemy.com/course/react-the-complete-guide-incl-redux/ 上的 React – The Complete Guide 课程,以及 YouTube 上的免费教程。
官方文档是另一个很好的探索之地,因为它包含了深入核心主题的深入探讨以及更多教程文章:react.dev/。
超越 React 网络应用
本书专注于使用 React 来构建网站。这有几个原因。首先,React 最初是为了简化构建复杂网络用户界面的过程而创建的,而且 React 正在为越来越多的网站提供动力。它是使用最广泛的客户端网络开发库之一,而且比以往任何时候都更受欢迎。
但是,学习如何使用 React 进行网页开发也是有意义的,因为你不需要额外的工具——只需一个文本编辑器和浏览器即可。
话虽如此,React 也可以用来构建浏览器外部的用户界面以及网站。借助 React Native 和为 React 定制的 Ionic,你拥有两个非常流行的项目库,它们使用 React 来构建针对 iOS 和 Android 的原生移动应用。
因此,在学完所有这些 React 基础知识之后,探索这些项目也很有意义。挑选一些 React Native 或 Ionic 课程(或使用官方文档),了解你如何使用本书中涵盖的所有 React 概念来构建可以分发到平台应用商店的真正原生移动应用。
React 可以用于构建各种平台的各种交互式用户界面。现在你已经完成了这本书,你拥有了使用 React 构建下一个项目的工具——无论它针对哪个平台。
最后的话
通过本书中讨论的所有概念,以及深入学习的额外资源和起点,你已充分准备好使用 React 构建功能丰富且高度用户友好的网络应用。
无论是一个简单的博客还是一个复杂的软件即服务解决方案,你现在都知道了构建用户喜爱的 React 驱动的网络应用所需的关键 React 概念。
我希望你能从这本书中获得很多收获。请分享你的任何反馈,例如,通过 X (@maxedapps) 或发送电子邮件至 customercare@packt.com。
加入我们的 Discord 社区
与其他用户、AI 专家和作者本人一起阅读这本书。
提出问题,为其他读者提供解决方案,通过“问我任何问题”的环节与作者聊天,等等。
扫描二维码或访问链接加入社区。