原文来源于:程序员成长指北;作者:oil欧哟
如有侵权,联系删除
前言
如果你是一名经验丰富的 react 开发者,那么你肯定有遇到过以下几种情况:
-
请求库封装复杂,手动实现各种缓存验证去重逻辑,还需要维护请求加载或错误状态
-
由于组件的重复渲染导致的 重复请求
-
用户将网站长时间挂在后台导致缓存中的 数据过期
-
请求方法写在很顶层的组件,将请求数据一层层传递给依赖的自组件使用,导致 组件 props 冗长
以上几种场景各自都有特殊的处理方式,例如为 axios 增加类似防抖的重复请求处理,计算用户无请求发送时间以确保数据更新,或者为了方便请求响应数据的传递引入庞大的状态管理库。
如果你认为这些方式相对比较复杂或者不够优雅,那么这篇文章带给你一个新的请求数据思路——SWR
SWR 是什么?
SWR 是 Next.js 背后的团队 vecel 开源的一个 用于数据请求的 React Hooks 库
官方介绍:“SWR” 这个名字来自于 stale-while-revalidate
:一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。
使用 SWR,组件将会不断地、自动获得最新数据流。
UI 也会一直保持快速响应。
SWR 的使用非常简单,下面是一个搭配 axios
进行请求的例子:
import axios from'axios'
constfetcher = url => axios.get(url).then(res => res.data)
functionApp () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
在这个例子中我们可以看到,我们使用 useSWR 这个 hook 发起一个请求,hook 接收两个参数:
-
第一个参数是请求的路径,同时它也作为一个 key 值用于缓存数据。
-
第二个参数是一个异步请求方法,它参数就是 hook 接收到的第一个参数,返回值为请求到的数据
这个 hook 的返回值也有两个,data
为 fetcher
中获取到的数据,error
则为请求失败时的错误。
useSWR 既然是一个 hook ,说明 data 已经是一个状态数据了,我们不需要再手动 useState 维护请求到数据,当 data 改变时 UI 会随着改变。
我们传统的请求方式可能大部分是这样子的:
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const getData = axios.get('/oiloil').then((res) => res.data);
useEffect(async () => {
setLoading(true);
const res = awaitgetData().catch(err=>{
//handle error
});
setData(res);
setLoading(false);
}, []);
上面的例子中我们得手动去维护请求数据和加载状态,而且 useEffect
中现在还没有写依赖,如果有时请求中依赖某些状态,那么这里的请求触发时机就会变得没那么可控了。
我们使用 useSWR 模拟上面的例子简单实现对比一下:
// useData.ts
constuseData = () => {
const { data, error ,mutate} = useSWR<any[]>(
"/oiloil",
(url: string) => axios.get(url).then((res) => res.data)
);
return {
data,
error,
isLoading: !error && !data,
reload: mutate,
};
}
exportdefault useData
// ComponentA.tsx
const {data, error, isLoading, reload} = useData
这里我单独抽离了一个 useData
这个自定义 hook 用于请求 /oiloil
这个接口的数据,当我们在组件中使用 hook 的时候就直接发送了请求,如果我们后面需要重复请求可以直接调用 reload
方法,而且通过 !error && !data
我们还可以获取到接口是否正在请求中这个状态。
这里虽然代码没有简短多少,但是我们的 useData
hook 是可以复用的,我们可以在任何组件中直接使用它来获取数据,不需要维护新的状态,而且如果 useData
的调用时机与 ComponentA
相同,它们会使用同一个状态,不需要进行重复请求,也不需要额外定义很多的组件 props
。
这两种请求方式的数据流如下图所示:
当然这里仅仅是 hook 带来的好处,下面我们详细讲讲 SWR 可以在我们实际开发的场景中提供什么帮助吧~
实际使用场景
数据缓存
首先就是 SWR 的核心功能 数据缓存 了。我们每一次发送请求后,后端响应的数据都会被缓存下来,当我们下一次请求相同接口时,SWR 依然会发送请求,但是它会先将上一次请求的数据直接给你,然后再去发送请求。
当新的请求结束,得到响应数据后,如果它与第一次请求的响应值不同,那么 SWR 就会直接更新 state
,这样你的 UI 也会渲染上最新的数据了。
下面是一张使用缓存前后页面渲染流程的对比图:
光看这张图你可能还比较难 get 到使用缓存的好处,下面我讲一个实际的场景:
在我们常见的表格组件中,最后一列往往都是用于一些删除或者编辑操作的,如下图:
当我们加载表格时,我们会发送请求以获取表格需要的数据,在请求的过程中我们可能会展示一个加载动画或者骨架屏。
如果我们在表格数据加载完成后,我们操作一下表格中的数据,例如删掉其中一条,此时在发送删除请求成功后,我们一般会重新请求一下表格的数据,那么此时 又会出现一次加载动画或者骨架屏。直到新的请求拿到后再渲染新数据。这样用户体验就没那么好。
但如果我们使用 SWR 的话,删除后不会进入加载状态,而是在重新请求表格数据后将表格渲染新的数据。对于用户来说就是我点击了删除后,那条数据直接消失了,而且还避免了表格在 有数据的情况与加载动画切换时 组件会快速闪一下的问题。
请求错误重试
接着就是 请求重试 了,大家可以尝试着搜一搜 axios 请求错误重试 这个关键字,可以在很多文章中看到大家对 aioxs 响应拦截器进行一些封装处理,实现当满足某种错误条件时进行错误重试,可以自己配置 重试次数 和 重试时延。 当然封装的方式是五花八门的。
而在 SWR 中,它本身自带了 错误重试 的功能的,当出现请求错误时,SWR 使用 指数退避算法 重发请求。该算法允许应用从错误中快速恢复,而不会浪费资源频繁地重试。错误重试的功能默认是开启的,当然你也可以手动关闭。
如果你不满足于 SWR 使用的指数退避算法,而是想要自己来控制请求的重试,那也非常简单。官方示例如下:
useSWR('/api/user', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 404 时不重试。
if (error.status === 404) return
// 特定的 key 时不重试。
if (key === '/api/user') return
// 最多重试 10 次。
if (retryCount >= 10) return
// 5秒后重试。
setTimeout(() =>revalidate({ retryCount: retryCount }), 5000)
}
})
上面的例子可以看到,我们通过 useSWR
第三个参数配置一个 onErrorRetry
函数,函数的参数中包含了一些请求信息以及重试次数,这样我们需要进行自定义错误重试的时候配置起来非常方便。
除了在单个请求中配置,你也可以通过 SWR 的全局配置,为所有的请求设置相同的策略。全局配置方式如下:
<SWRConfig value={options}>
<Component/>
</SWRConfig>
使用 SWRConfig
包裹在你的组件外层,一般我们会放在 App.tsx
中以保证包裹了所有的组件,然后在 value
中传入你的全局配置。
数据突变(mutate)
当我们调用 useSWR
这个 hook 时,它会自动为我们发送请求,例如我们刚刚进入页面时调用就会去获取渲染页面的初始数据,那如果我们知道当前页面的数据已经变更了要如何重新请求呢?
这里我们可以使用 useSWRConfig()
所返回的 mutate
函数,来广播重新验证的消息给其他的 SWR hook。使用同一个 key 调用 mutate(key)
即可。下面的官方提供的例子:
import useSWR, { useSWRConfig } from'swr'
functionApp () {
const { mutate } = useSWRConfig()
return (
<div>
<Profile />
<button onClick={() => {
// 将 cookie 设置为过期
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
// 告诉所有具有该 key 的 SWR 重新验证
mutate('/api/user')
}}>
Logout
</button>
</div>
)
}
mutate
的意思就是突变,我们调用 mutate
也就是在显式的告诉 swr 我的数据已经发生变化啦,赶紧给我更新一波。
你需要重新请求的 key
传入 mutate 方法即可,重新发送请求后如果数据发生了变更 swr 会为我们更新缓存并重新渲染,如果你需要特殊的处理也可以在第二个参数传入 options
选项,options
包含了以下几个配置项:
optimisticData
:立即更新客户端缓存的数据,通常用于 optimistic UI。
revalidate
:一旦完成异步更新,缓存是否重新请求。
populateCache
:远程更新的结果是否写入缓存,或者是一个以新结果和当前结果作为参数并返回更新结果的函数。
rollbackOnError
:如果远程更新出错,是否进行缓存回滚。
这里我们可以发现 mutate
方法如果只能通过 hook 的方式获取的话,我们就只能在 组件或者自定义 hook 中实现一些重新请求逻辑了,但有时我们需要在例如普通函数中触发重新请求该怎么办呢?
例如当我们 目前操作的用户权限突然被调低 了,在获取数据时后端响应了状态码 403
,我们想要在 axios 的响应拦截中配置一个:如果遇到状态码为 403
的响应数据就重新获取一下用户的权限以重新渲染页面,将一些当前用户权限不该显示的内容隐藏,我们可以这么实现:
import axios from'axios';
import { mutate } from'swr';
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
switch (error.response.status) {
case403: {
mutate('/user/me');
break;
}
case500: {
// ... do something
break;
}
default: {
// ... do something
}
}
}
returnPromise.reject(error);
}
);
将 mutate
函数直接从 swr
中引入,而不是使用 hook 的方式获取,这种方式也可以用来实现预请求数据。
更多使用姿势可以参考文档:swr.vercel.app/zh-CN/docs/…
Typescript 支持
SWR 的 typescipt 支持非常好,毕竟自身就是用 ts 实现的。如果我们想要在使用 hook 时为请求的响应值提供类型,只需要传入一个泛型就OK,如下例:
// 🔹 B. 指定 data 类型:
// `fetcher` 一般会返回 `any`.
const { data } = useSWR<User>('/api/user', fetcher)
当然你也可以直接在 Fetcher 中传入泛型,例如大家常用的 axios
,这样你在 Fetcher 中进行数据处理时也可以获得类型提示。
// 🔹 A. 使用一个有类型的 fetcher:
// `getUser` 是 `(endpoint: string) => User`.
const { data } = useSWR('/api/user', getUser)
推荐使用方式
经过一段时间的实际使用,我们在项目中将每个获取数据的请求根据 数据类型 进行分类,并以 hook 的方式进行二次封装:
import axios from'axios';
import useSWR from'swr';
import { UserResponse } from'types/User';
constuseUser = () => {
const { data, error, isLoading, isValidating, mutate } = useSWR<UserResponse>('/user', (url) =>
axios.get(url).then((res) => res.data.payload)
);
return {
data,
reload: mutate,
isLoading,
isValidating,
isError: error,
};
};
exportdefault useUser;
以上例子就是一个获取用户数据的一个 hook ,实际使用的过程中还会出现 hook 嵌套的情况,例如我需要获取用户的列表,再根据某个用户的 id 去获取相应的用户详情。
由于两个请求是有依赖关系的,我们需要先从 useUser
中获取用户 id 后再发送新的请求,那我们可以这么写:
import axios from'axios';
import useSWR from'swr';
import useUser from'./useUser';
constuseUserDetail = () => {
const { data } = useUser();
const { data, error, isLoading, isValidating, mutate } = useSWR(
data[0].id ? `users/${data[0].id}/detail` : null,
(url: string) => axios.get(url).then((res) => res.data.payload)
);
return {
data,
reload: mutate,
isLoading,
isValidating,
isError: error,
};
};
exportdefault useUserDetail;
useDetail
用于获取用户详情,这个 hook 中 useSWR 的 key
值是一个三目表达式,当 key
为 null
时,SWR 将不会发送请求,直到 key
有值才会发送请求,以确保请求间的依赖关系正常。
这里的 isLoading
表示目前暂无缓存,正在进行初次加载。 isValidating
则表示已经有缓存了,但是由于重新聚焦屏幕,或者手动触发数据更新数据重新验证的加载。
在实际使用时,例如表格加载的场景,初次进入表格我们可以判断 isLoading
来展示一个骨架屏:
而后续的表格刷新,如果我们不想每次刷新都变为骨架屏,而是展示一个简单的加载动画提升用户的使用体验,我们就可以使用 isValidating
:
这里额外提一点,如果你不想在表格每次加载都展示加载动画,比如只有在请求实践超过了 500ms
才响应时展示加载动画,你可以通过防抖来实现:
import { Center, Spinner } from'@chakra-ui/react';
import { useDebounce } from'ahooks';
import { memo } from'react';
constTableLoading: React.FC<{ isOpen: boolean }> = ({ isOpen }) => {
// To prevent the loading animation from flickering frequently, it will only be displayed if the loading time exceeds 500 ms
const debouncedLoading = useDebounce(isOpen, {
wait: 500,
});
return debouncedLoading ? (
<Center
>
<Spinner />
</Center>
) : null;
};
exportdefaultmemo(TableLoading);
这里直接使用了 ahook 的 useDebounce
hook,当 isOpen
变化后如果 500ms
后还没有变化就会展示加载动画,这样在网络流畅的情况下,用户几乎感知不到数据的加载,用户体验嘎嘎提升。
注意 hook 的执行时机,避免重复请求
这里我举个例子:假设页面中有一个表格,点击表格首个单元格可以弹出展示详情的弹窗如下图:
点击详情弹出弹窗:
我们可以通过如下伪代码简单实现下:
constPage = () => {
const { data } = useSWR(
"/api/table",
axios.get(url).then((res) => res.data.payload)
);
const [modalIsOpen, setModalIsOpen] = useState(false);
return (
<>
<table>{/* ...省略表格实现 */}</table>
<Modal isOpen={modalIsOpen} />
</>
);
};
constModal = ({ isOpen }) => {
const { data } = useSWR(
"/api/table",
axios.get(url).then((res) => res.data.payload)
);
// 在这里判断弹窗是否弹出
return isOpen && <div>{/* ...省略弹窗实现 */}</div>;
};
分析一下,这里我们在页面和 Modal
组件中都使用了 SWR 请求同一个数据,当页面渲染时,Modal
组件中的 useSWR
与页面中的 useSWR
几乎同时触发,在一定时间内重复的请求会被 SWR 删除,因此只会发送一个请求。
但是如果我们将控制弹窗是否显示的判断从 Modal
组件移到 Page
中,如下所示:
constPage = () => {
const { data } = useSwr(
"/api/table",
axios.get(url).then((res) => res.data.payload)
);
const [modalIsOpen, setModalIsOpen] = useState(false);
return (
<>
<table>{/* ...省略表格实现 */}</table>
// 移动到在这里判断弹窗是否弹出
{modalIsOpen && <Modal />}
</>
);
};
constModal = () => {
const { data } = useSwr(
"/api/table",
axios.get(url).then((res) => res.data.payload)
);
return<div>{/* ...省略弹窗实现 */}</div>;
};
原本只需要在渲染页面的时候获取一次数据。而修改后每次打开弹窗都会随着 Modal
组件的挂载和卸载重新执行 Modal
组件内的 useSwr
方法,造成重复请求,如果你的 hook 还是嵌套使用的,那么重复请求的数量就更大了。
这里需要注意一下,在 React 官方文档中提到了 hooks-rules :
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。
这个规则其实与上述的例子没有太大关联,React 文档中的规则是为了 避免 state 混乱,而上面的例子则是告诉大家 调用 useSWR 要尽量在同一个时机以避免重复请求 ,大家不要混淆了。
在实际项目中,由于业务逻辑复杂,不可能像上面的代码这么清晰,因此在开发和 review 的过程中要谨慎,避免踩坑。
总结
这篇文章介绍了 SWR 的的优势及使用场景,它非常适合例如 SaaS 产品或者后台管理系统这种对于数据实时性有一定要求的项目。