从各种 API 有效地获取数据对于现在的几乎每一个 Web 应用来说都至关重要。
可能一开始,你会使用内置的 fetch
或 Axios
手动获取数据,但很快你就会遇到如处理缓存、错误处理和过时数据等挑战。随着你的应用程序的增长,管理状态和处理复杂的数据获取逻辑变得更加具有挑战性和耗时。
为了解决这些问题,创建了 SWR。SWR 代表“stale-while-revalidate”,它是一个强大的 React 数据获取库,提供了一种简单而优雅的方式来处理数据获取、缓存和错误处理,同时保持高性能。此外,它使我们能够使用 TypeScript,这是 JavaScript 的静态类型超集,可以提供额外的类型安全层。
在本文中,我们将深入探讨如何使用 SWR 和 TypeScript 在 React 中掌握数据获取,探索 SWR 的核心概念,并演示如何在 SWR 中使用 TypeScript 来处理数据获取、缓存和错误处理,同时保持高性能。我们还将提供实际示例和最佳实践,以便在你的 React 应用程序中进行高效和类型安全的数据获取,以及讨论像 React Suspense 这样的前沿特性在数据获取中的使用。
理解 SWR 以及它在幕后的工作方式。
为了正确理解 SWR 到底是什么,我们从一个例子开始。
想象你在一个图书馆工作,为人们提供书籍。有时,人们会多次要求同一本书,这对你来说是一个耗时且重复的任务。如果你不得不一次又一次地从同一个地方拿书,你会花费大量的时间做同样的重复工作,这会给你的客户带来糟糕的体验,因为他们必须等你完成。
为了解决这个问题,你可以在一定的时间内记录下被请求的书籍,并将它们放在附近。这样,当人们要求同一本书时,你就可以立即提供,比每次都要寻找这本书要少费力,也更快。
在这个例子中,SWR 作为你的图书馆助手参与游戏,帮助你跟踪哪些书籍被频繁请求,并确保你只在需要的时候去书架上。
我们将在下一节深入介绍如何在 React 中使用 SWR,但是,对于这个解释,需要引入一些概念。
要在 React 中使用 SWR,我们需要使用一个名为 useSWR
的钩子,它根据 fetcher
函数的状态返回一些变量(在我们的例子中,这个函数将代表去书架上拿书的动作)。
const fetcher = (...args) => fetch(...args).then(res => res.json())
const {data, error, isLoading, isValidating} = useSWR(key, fetcher);
我们可以按照以下方式定义这些变量:
- data: 此变量代表书本本身。
- error: 如果由于某种原因你找不到这本书,此变量将包含原因。
- isLoading: 当你在去拿一本书的路上时,这个变量将有一个布尔值 true 。
- isValidating: 这个变量将类似于
isLoading
,但是当你检查你的记录以确认你身边是否已经有这本书时,它的值将为true
。从现在开始,你身边的这个位置将被称为cache
。 - key: 代表客户想要的书。例如,它可能是标题。
我们可以在下一个图表中清楚地看到这一点。
在底层,SWR 使用缓存来存储已经获取过的数据。在我们的例子中,这将是最近的书架。
当用户请求数据时,SWR 首先检查缓存,看看是否已经有了数据。如果有,它会立即返回缓存的数据。如果没有,它会向服务器发送请求,获取数据,一旦数据返回,它会将其存储在缓存中以备将来使用。
正如我们之前提到的,SWR 代表 "stale-while-revalidate"(陈旧数据重新验证)。这是一种高级的说法,表示它允许我们在应用程序在后台重新验证的同时向用户显示陈旧的数据。这意味着,如果用户多次请求同一本书,他们将立即看到缓存的数据,而不是每次都等待响应。
好的,说得够多了,给我看些代码。
如何在 React 和 TypeScript 中使用 SWR。
注意:本文中使用的代码库可以在这里找到,GitHub 仓库。
一旦我们有了我们的项目,使用 SWR 的第一步是将其安装为依赖项。除了 SWR,我还将使用 Axios 。要将两者都安装为依赖项,请在终端中运行此命令。
yarn add swr axios
# or
npm install swr axios
一旦我们安装了依赖项,我们需要创建一个 fetcher
函数,如前所述。
对于这个例子,PokeApi 将会很有用,因为它提供了大量的数据集以及其他有趣的功能。
// src/api/index.ts
import axios from 'axios';
export const api = axios.create({
baseURL: 'https://pokeapi.co/api/v2/',
});
export const fetcher = (url: string) => api.get(url).then((res) => res.data);
这个函数应作为我们的 useSWR
钩子的第二个参数传递。然而,在我们的情况下,我们将使用配置上下文来全局配置我们的 SWR 实例。为此,我们可以从同一库中导入一个 SWRConfig
上下文,并将其包裹在我们的整个应用程序中。这样,每次我们在应用程序中使用 useSWR 钩子时,它都会有相同的配置。
import React from 'react';
import { createRoot } from 'react-dom/client';
import { SWRConfig } from 'swr';
import { fetcher } from './api';
import './index.css';
const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<SWRConfig
value={{
fetcher
}}
>
<App />
</SWRConfig>
</React.StrictMode>
);
我们现在可以在我们的应用程序中获取数据了。为此,我们将使用我们已经在我们的 App 组件中配置的 SWR 钩子。为了增加类型安全性,我们将使用 TypeScript 泛型来使其更好。
export const PokemonGrid = () => {
const { data, isLoading } = useSWR<PokemonResponse>('pokemon?limit=20');
if (isLoading) return <div>Loading...</div>;
if (!data) throw new Error();
return (
<div className='p-10'>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 w-full mb-8'>
{data.results.map(({ name }, index) => (
<PokemonCard key={name} name={name} index={index + 1} />
))}
</div>
</div>
);
};
使用 isLoading
指示器,我们可以显示一个加载状态,以便在从服务器获取数据时给我们的用户立即反馈,而且,多亏了 TypeScript,我们获取的数据完全按照我们需要构建用户界面的属性进行了类型化。
这是我们的 PokemonGrid 组件的最终结果。
现在我们已经基本了解了如何在 React 中使用 SWR 进行数据获取,让我们继续讨论更激动人心的话题吧。
在 React 中进行下一级数据获取:结合 SWR 和 React Suspense
在 React 18 中,我们可以在诸如 Next 或 Remix 之类的有自己见解的框架中使用 Suspense 进行数据获取。然而,目前还不建议将其集成到像 SWR 这样的数据框架中,因为它是一个实验性的特性,其 API 在未来可能会发生变化。
尽管 React Suspense 仍被视为实验性功能,但它有可能简化 React 应用中的数据获取,并通过与 SWR 的集成,我们可以在你的应用中实现更高性能和无缝的数据获取,而无需担心处理加载状态。
要开始使用 Suspense,我们需要在我们的配置上下文中启用该选项(注意我们已经包含了一些额外的东西,如路由器,这只是为了实现额外的示例)。
import React, { Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { SWRConfig } from 'swr';
import { fetcher } from './api';
import { router } from './router';
import { PageSkeleton, DashboardBroken } from './components';
import './index.css';
const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<ErrorBoundary fallback={<DashboardBroken />}>
<Suspense fallback={<PageSkeleton />}>
<SWRConfig
value={{
fetcher,
suspense: true,
}}
>
<RouterProvider router={router} />
</SWRConfig>
</Suspense>
</ErrorBoundary>
</React.StrictMode>
);
一旦我们开始使用 Suspense
,由我们的钩子返回的 data
将始终是获取响应(所以不再需要检查它是否正在加载)。但是,如果发生错误,你需要使用一个 ErrorBoundary 来捕获它,而我们等待数据时,提供的 fallback
属性下的组件将被渲染。
有了这个,我们的 PokemonGrid 将会是这样的:
export const PokemonGrid = () => {
const { data } = useSWR<PokemonResponse>('pokemon?limit=20');
return (
<div className='p-10'>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 w-full mb-8'>
{data?.results.map(({ name }, index) => (
<PokemonCard key={name} name={name} index={index + 1} />
))}
</div>
</div>
);
};
正如你所看到的,我们不再需要处理加载状态,因为 Suspense 会暂停我们组件的渲染,直到数据准备就绪,然后渲染备用组件。
这样,我们可以处理一个很好的加载骨架,以便在等待数据时给我们的用户提供即时反馈。
这还没有完成,正如我们之前提到的,如果出现问题,我们可以使用错误边界轻松处理错误,以提供非常好的用户体验。
我们已经配置了我们的错误边界回退,所以,如果出现错误,我们的用户将看到下一个屏幕。
使用 SWR 预取功能预取数据。
到目前为止,我们已经为我们的用户提供了非常好的功能,如提供加载状态和处理错误,但如果我们使用另一个 SWR 提供的资源 prefetching
,我们可以使其更好。这使我们可以在用户请求数据之前开始获取数据,从而提高我们应用程序的性能。在我们的例子中,等价的情况是,如果你提前知道你的客户想要什么书,那么你可以在他们要求之前抓取它们。
为了向你展示如何使用这个资源,我已经使我们的 PokemonCard 可以点击重定向到一个显示宝可梦详情的新页面。
如果没有预取,我们的用户将不得不等待新数据的获取。好在我们可以包含一个事件处理器来检测用户的鼠标何时在 PokemonCard 上,并在那一刻开始获取数据。这样,当用户导航到 PokemonDetails
页面时,数据将已经准备好。
为了实施这个改进,我们需要在我们的卡片组件中做一些调整。
import { preload } from 'swr';
import { fetcher } from '../api';
import { Link } from 'react-router-dom';
interface PokemonCardProps {
name: string;
index: number;
}
export const PokemonCard = ({ name, index }: PokemonCardProps) => {
const indexToShow =
index < 10 ? `00${index}` : index === 10 ? `0${index}` : index < 100 ? `0${index}` : index;
const onHover = () => {
preload(`pokemon/${index}`, fetcher);
};
return (
<Link to={`${index}`}>
<div
className='bg-slate-600 rounded-lg p-4 hover:cursor-pointer hover:bg-slate-950'
onMouseEnter={onHover}
>
<img
className='w-full'
src={`https://assets.pokemon.com/assets/cms2/img/pokedex/detail/${indexToShow}.png`}
alt={name}
/>
<div className='capitalize text-lg'>{name}</div>
</div>
</Link>
);
};
如你所见,我们可以从 SWR 导入预加载,一旦用户悬停在卡片上就开始获取。
这种改进带来了巨大的性能提升,最好的是,如果你将鼠标悬停在同一个宝可梦上,数据不会再次获取,因为它已经在缓存中,所有这些都由 SWR 自动处理。太棒了 😎
使用 SWR 预取特性来实现数据分页。
在 React 应用中,实现分页带来了一些挑战,如管理加载状态,预获取数据,以及保持更新的缓存。如果你做到了这一点,你就会知道这些是我们首先使用 SWR 的原因,分页并不例外,所以,让我们直接跳入主题。
第一步是包含一个状态,让 React 在页面号码改变时渲染新的内容,以及一些修改它的函数。之后,我们包含两个按钮来处理页面之间的导航,当用户悬停在它们上面时,会触发一个预获取函数。
import useSWR, { preload } from 'swr';
import { useState } from 'react';
import { PokemonResponse } from '../types/pokemon';
import { PokemonCard } from './PokemonCard';
import { fetcher } from '../api';
type Direction = 'prev' | 'next';
const INITIAL_PAGE = 0;
export const PokemonGrid = () => {
const [page, setPage] = useState(INITIAL_PAGE);
const { data } = useSWR<PokemonResponse>(`pokemon?limit=20&offset=${page * 20}`);
const onPrevPage = () => setPage((prev) => prev - 1);
const onNextPage = () => setPage((prev) => prev + 1);
const onHover = (direction: Direction) => {
if (direction === 'prev') preload(`pokemon?limit=20&offset=${(page + 1) * 20}`, fetcher);
if (direction === 'next' && page !== INITIAL_PAGE)
preload(`pokemon?limit=20&offset=${(page - 1) * 20}`, fetcher);
};
return (
<div className='p-10'>
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 w-full mb-8'>
{data?.results.map(({ name }, index) => (
<PokemonCard key={name} name={name} index={index + 1 + page * 20} />
))}
</div>
<div className='flex justify-center gap-4'>
<button
className='w-20 rounded bg-slate-500 p-4 hover:bg-black disabled:cursor-not-allowed'
onClick={onPrevPage}
disabled={page === INITIAL_PAGE}
onMouseEnter={() => onHover('prev')}
>
Prev
</button>
<button
className='w-20 rounded bg-slate-500 p-4 hover:bg-black '
onClick={onNextPage}
onMouseEnter={() => onHover('next')}
>
Next
</button>
</div>
</div>
);
};
这就是结果。现在你可以通过 SWR 平滑地浏览数千个宝可梦。
总结
总的来说,数据获取是构建高性能和用户友好的 React 应用程序的一个重要方面。通过使用 SWR 和 TypeScript,开发者可以利用缓存、预获取和高级数据获取技术的力量,简化他们的数据获取代码并实现下一级的性能。此外,通过集成 React Suspense,开发者可以进一步提升他们的数据获取能力,并提供无缝的用户体验。
交流
首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。