查看原文
其他

都什么时代还在发传统请求?来看看 SWR 如何用 React Hook 实现优雅请求

作者:oil欧哟

https://juejin.cn/post/7197673814179119162

前言

如果你是一名经验丰富的 react 开发者,那么你肯定有遇到过以下几种情况:

  • 请求库封装复杂,手动实现各种缓存验证去重逻辑,还需要维护请求加载或错误状态
  • 由于组件的重复渲染导致的 重复请求
  • 用户将网站长时间挂在后台导致缓存中的 数据过期
  • 请求方法写在很顶层的组件,将请求数据一层层传递给依赖的自组件使用,导致 组件 props 冗长

以上几种场景各自都有特殊的处理方式,例如为 axios 增加类似防抖的重复请求处理,计算用户无请求发送时间以确保数据更新,或者为了方便请求响应数据的传递引入庞大的状态管理库。

如果你认为这些方式相对比较复杂或者不够优雅,那么这篇文章带给你一个新的请求数据思路——SWR

SWR 是什么?

SWR[1] 是 Next.js 背后的团队 vecel 开源的一个 用于数据请求的 React Hooks 库

官方介绍:“SWR” 这个名字来自于 stale-while-revalidate:一种由 HTTP RFC 5861[2] 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。

使用 SWR,组件将会不断地自动获得最新数据流。
UI 也会一直保持快速响应

SWR 的使用非常简单,下面是一个搭配 axios 进行请求的例子:

import axios from 'axios'

const fetcher = url => axios.get(url).then(res => res.data)

function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

在这个例子中我们可以看到,我们使用 useSWR 这个 hook 发起一个请求,hook 接收两个参数:

  • 第一个参数是请求的路径,同时它也作为一个 key 值用于缓存数据。
  • 第二个参数是一个异步请求方法,它参数就是 hook 接收到的第一个参数,返回值为请求到的数据

这个 hook 的返回值也有两个,datafetcher 中获取到的数据,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 = await getData().catch(err=>{
     //handle error
  });
  
  setData(res);
  setLoading(false);
}, []);

上面的例子中我们得手动去维护请求数据和加载状态,而且 useEffect 中现在还没有写依赖,如果有时请求中依赖某些状态,那么这里的请求触发时机就会变得没那么可控了。

我们使用 useSWR 模拟上面的例子简单实现对比一下:

// useData.ts

const useData = () => {

const { data, error ,mutate} = useSWR<any[]>(
    "/oiloil",
    (url: string) => axios.get(url).then((res) => res.data)
  );
  
  return {
    data,
    error,
    isLoading: !error && !data,
    reload: mutate,
  };
}

export default 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 使用 指数退避算法[3] 重发请求。该算法允许应用从错误中快速恢复,而不会浪费资源频繁地重试。错误重试的功能默认是开启的,当然你也可以手动关闭。

如果你不满足于 SWR 使用的指数退避算法,而是想要自己来控制请求的重试,那也非常简单。官方示例如下:

useSWR('/api/user', fetcher, {
  onErrorRetry(error, key, config, revalidate, { retryCount }) => {
    // 404 时不重试。
    if (error.status === 404return

    // 特定的 key 时不重试。
    if (key === '/api/user'return

    // 最多重试 10 次。
    if (retryCount >= 10return

    // 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'

function App () {
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) {
case 403: {
mutate('/user/me');
break;
}
case 500: {
// ... do something
break;
}
default: {
// ... do something
}
}
}
return Promise.reject(error);
}
);

mutate 函数直接从 swr 中引入,而不是使用 hook 的方式获取,这种方式也可以用来实现预请求数据。

更多使用姿势可以参考文档:https://swr.vercel.app/zh-CN/docs/mutation

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';

const useUser = () => {
  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,
  };
};

export default useUser;

以上例子就是一个获取用户数据的一个 hook ,实际使用的过程中还会出现 hook 嵌套的情况,例如我需要获取用户的列表,再根据某个用户的 id 去获取相应的用户详情。

由于两个请求是有依赖关系的,我们需要先从 useUser 中获取用户 id 后再发送新的请求,那我们可以这么写:

import axios from 'axios';
import useSWR from 'swr';

import useUser from './useUser';

const useUserDetail = () => {
  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,
  };
};

export default useUserDetail;

useDetail 用于获取用户详情,这个 hook 中 useSWR 的 key 值是一个三目表达式,keynull 时,SWR 将不会发送请求,直到 key 有值才会发送请求,以确保请求间的依赖关系正常。

这里的 isLoading 表示目前暂无缓存,正在进行初次加载。isValidating 则表示已经有缓存了,但是由于重新聚焦屏幕,或者手动触发数据更新数据重新验证的加载。

在实际使用时,例如表格加载的场景,初次进入表格我们可以判断 isLoading 来展示一个骨架屏:

而后续的表格刷新,如果我们不想每次刷新都变为骨架屏,而是展示一个简单的加载动画提升用户的使用体验,我们就可以使用 isValidating

这里额外提一点,如果你不想在表格每次加载都展示加载动画,比如只有在请求实践超过了 500ms 才响应时展示加载动画,你可以通过防抖来实现:

import { Center, Spinner } from '@chakra-ui/react';
import { useDebounce } from 'ahooks';
import { memo } from 'react';

const TableLoading: 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;
};

export default memo(TableLoading);


这里直接使用了 ahook 的 useDebounce hook,当 isOpen 变化后如果 500ms 后还没有变化就会展示加载动画,这样在网络流畅的情况下,用户几乎感知不到数据的加载,用户体验嘎嘎提升。

注意 hook 的执行时机,避免重复请求

这里我举个例子:假设页面中有一个表格,点击表格首个单元格可以弹出展示详情的弹窗如下图:

点击详情弹出弹窗:

我们可以通过如下伪代码简单实现下:

const Page = () => {
const { data } = useSWR(
"/api/table",
axios.get(url).then((res) => res.data.payload)
);
const [modalIsOpen, setModalIsOpen] = useState(false);

return (
<>
<table>{/* ...省略表格实现 */}</table>
<Modal isOpen={modalIsOpen} />
</>
);
};

const Modal = ({ 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 中,如下所示:

const Page = () => {
const { data } = useSwr(
"/api/table",
axios.get(url).then((res) => res.data.payload)
);
const [modalIsOpen, setModalIsOpen] = useState(false);

return (
<>
<table>{/* ...省略表格实现 */}</table>

// 移动到在这里判断弹窗是否弹出
{modalIsOpen && <Modal />}
</>
);
};

const Modal = () => {
const { data } = useSwr(
"/api/table",
axios.get(url).then((res) => res.data.payload)
);
return <div>{/* ...省略弹窗实现 */}</div>;
};

原本只需要在渲染页面的时候获取一次数据。而修改后每次打开弹窗都会随着 Modal 组件的挂载和卸载重新执行 Modal 组件内的 useSwr 方法,造成重复请求,如果你的 hook 还是嵌套使用的,那么重复请求的数量就更大了。

这里需要注意一下,在 React 官方文档中提到了 hooks-rules[4]

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。

这个规则其实与上述的例子没有太大关联,React 文档中的规则是为了 避免 state 混乱,而上面的例子则是告诉大家 调用 useSWR 要尽量在同一个时机以避免重复请求 ,大家不要混淆了。

在实际项目中,由于业务逻辑复杂,不可能像上面的代码这么清晰,因此在开发和 review 的过程中要谨慎,避免踩坑。

总结

这篇文章介绍了 SWR 的的优势及使用场景,它非常适合例如 SaaS 产品或者后台管理系统这种对于数据实时性有一定要求的项目。

在写文章的过程中 SWR 发布了新版本 SWR 2.0 发布[5],新增了很多特性,但没有中文翻译,因此我也为它们的文档贡献了一些中文翻译的 PR ,其中也包括了这篇 理解 SWR[6]。大家在使用的时候也可以看看,加深下理解 ,希望中文文档能降低大家的使用成本,使这个优秀的库可以在国内的流传度更高些。

如果文章对你有帮助欢迎关注点赞


参考资料

[1]

https://swr.vercel.app/zh-CN: https://link.juejin.cn?target=https%3A%2F%2Fswr.vercel.app%2Fzh-CN

[2]

https://tools.ietf.org/html/rfc5861: https://link.juejin.cn?target=https%3A%2F%2Ftools.ietf.org%2Fhtml%2Frfc5861

[3]

https://en.wikipedia.org/wiki/Exponential_backoff: https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FExponential_backoff

[4]

https://zh-hans.reactjs.org/docs/hooks-rules.html: https://link.juejin.cn?target=https%3A%2F%2Fzh-hans.reactjs.org%2Fdocs%2Fhooks-rules.html

[5]

https://swr.vercel.app/zh-CN/blog/swr-v2: https://link.juejin.cn?target=https%3A%2F%2Fswr.vercel.app%2Fzh-CN%2Fblog%2Fswr-v2

[6]

https://swr.vercel.app/zh-CN/docs/advanced/understanding: https://link.juejin.cn?target=https%3A%2F%2Fswr.vercel.app%2Fzh-CN%2Fdocs%2Fadvanced%2Funderstanding

向下滑动查看

推荐阅读  点击标题可跳转

1、2023 年了,为什么还不用 SWR ?

2、为什么你非常不适应 TypeScript

3、一文解锁 PDF 文件:使用 JavaScript 和 Canvas 渲染 PDF 内容

继续滑动看下一个
大前端技术之路
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存