找回密码
 立即注册
首页 业界区 业界 你应该了解的hooks式接口编程 - useSWR

你应该了解的hooks式接口编程 - useSWR

能杜孱 3 天前
什么是 useSWR ?

听名字我们都知道是一个 React 的 hooks,SWR 是stale-while-revalidate的缩写,  stale 的意思是陈旧的, revalidate 的意思是重新验证/使重新生效, 合起来的意识可以理解成 在重新验证的过程中先使用陈旧的,在http 请求中意味着先使用已过期的数据缓存,同时请求新的数据去刷新缓存。
这在 http 请求中Cache-Control响应头中已经实现,比如:
  1. Cache-Control: max-age=60, stale-while-revalidate=3600
复制代码
这意味着在缓存过期时间为60秒,当缓存过期时,你请求了该接口,并且在缓存过期后的3600内,会先使用原来过期的缓存作为结果返回,同时会请求服务器去刷新缓存。
示例:
未使用 swr 的情况,缓存过期后直接重放304协商缓存请求
1.gif

使用 swr 的情况,缓存过期后直接返回200过期的缓存数据,再进行304协商缓存请求
2.gif

但通过 nginx 等网关层来实现 swr ,没法做到接口缓存的精确控制,并且即使revalidate后的fresh数据返回了,也没法让页面重新渲染,只能等待下次接口请求。
useSWR直接在前端代码层实现了http请求SWR缓存的功能。
使用方式

在传统模式下,我们会这样写一个数据请求, 需要通过定义多个状态来管理数据请求, 并在副作用中进行指令式的接口调用。
  1. import { useEffect, useState } from "react";
  2. import Users from "./Users";
  3. export default function App() {
  4.   const [users, setUsers] = useState([]);
  5.   const [isLoading, setLoading] = useState(false);
  6.   const getUsers = () => {
  7.     setLoading(true);
  8.     fetch("/api/getUsers")
  9.       .then((res) => res.json())
  10.       .then((data) => {
  11.          setUsers(data);
  12.       })
  13.       .finally(() => {
  14.          setLoading(false)
  15.       })
  16.   }
  17.   useEffect(() => {
  18.     getUsers();
  19.   }, []);
  20.   return (
  21.    
  22.       {isLoading && <h2>loading... </h2>}
  23.       <UserList users={users} />
  24.    
  25.   );
  26. }
复制代码
使用 useSWR 后, 我们只需要告诉 SWR 这个请求的唯一 key, 与如何处理该请求的 fetcher 方法,在组件挂载后会自动进行请求
  1. import useSWR from "swr";
  2. import Users from "./Users";
  3. const fetcher = (...args) => fetch(...args).then((res) => res.json())
  4. export default function App() {
  5.   const { data: users, isLoading, mutate } = useSWR('/api/getUsers', fetcher)
  6.   return (
  7.    
  8.       {isLoading && <h2>loading... </h2>}
  9.       <UserList users={users} />
  10.    
  11.   );
  12. }
复制代码
useSWR的入参

  • key: 请求的唯一key,可以为字符串、函数、数组、对象等
  • fetcher:(可选)一个请求数据的 Promise 返回函数
  • options:(可选)该 SWR hook 的选项对象
key 会作为入参传递给 fetcher 函数,  一般来说可以是请求的URL作为key。可以根据场景自定义 key 的格式,比如我有额外请求参数,那么就把 key 定义成一个数组 ['/api/getUsers', { pageNum: 1 }], SWR会在内部自动序列化 key 值,以进行缓存匹配。
useSWR的返回

  • data: 通过 fetcher 处理后的请求结果, 未返回前为 undefined
  • error: fetcher 抛出的错误
  • isLoading: 是否有一个正在进行中的请求且当前没有“已加载的数据“。
  • isValidating: 是否有请求或重新验证加载
  • mutate(data?, options?): 更改缓存数据的函数
核心

全局缓存机制

我们每次使用 SWR 时都有用到 key,这将作为唯一标识将结果存入全局缓存中,这种默认缓存的行为其实非常有用。
例如获取用户列表在我们产品中是一个非常频繁的请求,细分下来用户列表都会有很多个接口
我们在写需求时,可能不知道这个接口数据有没有往 redux 中存过,并且往 redux 中放数据是个相对麻烦的操作有管理成本,那么大多数人的做法就是那有地方用,我就重新请求一遍。
例如一个模态框里存在用户列表,每次打开都要请求一次 (带远程搜索),用户每次都需要等待,当然你也可以把用户列表状态提升到模态框外面,但对应的就会有取舍,外部父组件其实根本不关心用户列表状态。
请求状态区分
当第一次请求,也就是没有找到对应 key 的缓存时,那么就会立即发起请求,isLoading 与 isValidating 都为 true。
当第二次请求时,有缓存,那么先拿缓存数据渲染,再进行请求,isValidating 为 true。
也就是说只要正在请求中,就是 isValidating,  无缓存数据且正在请求时才为isLoading状态。
3.gif

对应的状态图:
4.png

以上的案例中都是 key 为固定值的情况,但更多场景下 key 值会由于请求参数的变动而变动。
如一个搜索用户的 key 这样定义
  1. const [search, setSearch] = useState('');
  2. const { data } = useSWR(['/api/users', search], fetcher)
复制代码
每次输入都会导致 key 变化,key 变化默认就会重新请求接口,但其实 key 变化了也就代表了数据不可信了,需要里面重置数据,因此 data 会被立即重置为 undfined 。如果新的 key 已经有缓存值,那么也会先拿缓存值进行渲染。
5.gif

那么其实我们几乎什么额外代码也没加,就实现了一个自带数据缓存的用户搜索功能。
对应的key变化时的状态图:
6.png

假如我们偏要保留 key 变化前的数据先展示呢?因为我们还是会看到短暂的no-data
我们主要在第三个参数 options 中加入配置项 keepPreviousData 即可实现
7.png

实现效果与我们 gitlab 搜索分支时其实是一致的
8.gif

key变化且保留数据的状态图:
9.png

联动请求与手动触发

很多情况下接口请求都依赖于另外一个接口请求的结果,或者在某种情况下才发起请求。
首先如何让组件挂载时不进行请求,有三种方法
配置项实现

设置 options 参数 revalidateOnMount , 这种方法如果已有缓存数据,仍然会拿缓存数据渲染
10.gif

依赖模式

给定 key 时返回 falsy 值 或者 提供函数并抛出错误
  1. const { data } = useSWR(isMounted ? '/api/users' : null, fetcher)
  2. const { data } = useSWR(() => isMounted ? '/api/users' : null, fetcher)
  3. // 抛出错误
  4. const { data: userInfo } = useSWR('/api/userInfo')
  5. const { data } = useSWR(() => '/api/users?uid=' + userInfo.id, fetcher)
复制代码
那我们实现一个业务场景:数据源-数据库-数据表的联动请求
11.gif

我们几乎以一种自动化的方式实现了联动请求。
以下是代码示例:
  1. const DependenceDataSource = () => {
  2.     const [form] = Form.useForm();
  3.     const dataSourceId = Form.useWatch("dataSourceId", form);
  4.     const dbId = Form.useWatch("dbId", form);
  5.     const { data: dataSourceList = [], isValidating: isDataSourceFetching } =
  6.         useSWR({ url: "/getDataSource" }, dataSourceFetcher);
  7.         
  8.     const { data: dbList = [], isValidating: isDatabaseFetching } = useSWR(
  9.         () =>
  10.             dataSourceId
  11.                 ? { url: "/getDatabase", params: { dataSourceId } }
  12.                 : null,
  13.         databaseFetcher
  14.     );
  15.     const { data: tableList = [], isValidating: isTableFetching } = useSWR(
  16.         () =>
  17.             dataSourceId && dbId
  18.                 ? { url: "/getTable", params: { dataSourceId, dbId } }
  19.                 : null,
  20.         tableFetcher
  21.     );
  22.     return (
  23.         <Form
  24.             form={form}
  25.             style={{width: 400}}
  26.             layout="vertical"
  27.             onValuesChange={(changedValue) => {
  28.                 if ("dataSourceId" in changedValue) {
  29.                     form.resetFields(["dbId", "tableId"]);
  30.                 }
  31.                 if ("dbId" in changedValue) {
  32.                     form.resetFields(["tableId"]);
  33.                 }
  34.             }}
  35.         >
  36.             <Form.Item name="dataSourceId" label="数据源">
  37.                 <Select
  38.                     placeholder="请选择数据源"
  39.                     options={dataSourceList}
  40.                     loading={isDataSourceFetching}
  41.                     allowClear
  42.                 />
  43.             </Form.Item>
  44.             <Form.Item name="dbId" label="数据库">
  45.                 <Select
  46.                     placeholder="请选择数据库"
  47.                     options={dbList}
  48.                     loading={isDatabaseFetching}
  49.                     allowClear
  50.                 />
  51.             </Form.Item>
  52.             <Form.Item name="tableId" label="数据表">
  53.                 <Select
  54.                     placeholder="请选择数据表"
  55.                     options={tableList}
  56.                     loading={isTableFetching}
  57.                     allowClear
  58.                 />
  59.             </Form.Item>
  60.         </Form>
  61.     );
  62. };
复制代码
采用手动挡模式

使用上面这种方法利用了 key 变化会自动revalidate数据的机制实现了联动,但是有个非常大的弊端,你需要把 key 中所有的依赖参数都提取为state使组件能够重新 render 以进行revalidate。有点强制你使用受控模式的感觉,这会造成性能问题。
所以我们需要利用mutate进行手动请求, mutate(key?, data, options),
你可以直接从 swr 全局引入mutate方法,也可以使用 hooks 返回的 mutate 方法。
区别:

  • 全局mutate需要额外提供 key
  • hooks 内mutate直接绑定了key
  1. // 全局使用
  2. import { mutate } from "swr"
  3. function App() {
  4.   mutate(key, data, options)
  5. }
  6. // hook使用
  7. const UsersMutate = () => {
  8.     const { data, mutate } = useSWR({ url: "/getNewUsers" }, fetcher, {
  9.         revalidateOnFocus: false,
  10.         dedupingInterval: 0,
  11.         revalidateOnMount: false
  12.     });
  13.     return (
  14.         
  15.             <Input.Search
  16.                 onSearch={(value) => {
  17.                     mutate([{ id: 3, name: "user_" + value }]);
  18.                 }}
  19.             />
  20.             <List style={{ width: 300 }}>
  21.                 {data?.map((user) => (
  22.                     <List.Item key={user.id}>{user.name}</List.Item>
  23.                 ))}
  24.             </List>
  25.         
  26.     );
  27. }
复制代码
mutate 后会立马使用传入的 data 更新缓存,然后会再次进行一次 revalidate 数据刷新
12.gif

使用全局mutate传入 key { url: "/getNewUsers" } 后能够实现一样的效果,并且使用全局mutate
传入的 key 为函数时,你可以批量清除缓存。注意: mutate 中 key 传入函数表示过滤函数,与 useSWR 中传入 key 函数意义不同。
  1. mutate(
  2.     (key) => typeof key === 'object' && key.api === getUserAPI && key.params.search !== '',
  3.     undefined,
  4.   {
  5.     revalidate: false
  6.   }
  7. );
复制代码
但是,我们可以注意到现在传入的key是不带有请求参数的,hooks中mutate也无法修改绑定的key值,那么怎么携带请求参数呢?
useSWRMutation

useSWRMutation为一种手动模式的 SWR,只能通过返回的trigger方法进行数据更新。
这意味着:

  • 它不会自动使用缓存数据
  • 它不会自动写入缓存(可以通过配置修改默认行为)
  • 不会在组件挂载时或者 key 变化时自动请求数据
它的函数返回稍有不同:
  1. const { data, isMutating, trigger, reset, error } = useSWRMutation(
  2.     key,
  3.     fetcher, // fetcher(key, { arg })
  4.     options
  5. );
  6. trigger('xxx')
复制代码
useSWRMutation 的fetcher函数额外可以传递一个arg参数,  在 trigger可以中传递该参数,那么我们再来实现2.1依赖模式中的依赖联动请求。

  • 定义三个 fetcher, 接收参数, 这里参数不晓得为啥一定设计成{ arg }形式
  1. const dataSourceFetcher = (key) => {
  2.     return new Promise((resolve) => {
  3.         request(key).then((res) => resolve(res))
  4.     });
  5. };
  6. const databaseFetcher = (key, { arg }: { arg: DatabaseParams }) => {
  7.     return new Promise((resolve) => {
  8.         const { dataSourceId } = arg;
  9.         if (!dataSourceId) return resolve([])
  10.         request(key, { dataSourceId }).then((res) => resolve(res))
  11.     });
  12. };
  13. const tableFetcher = (key, { arg }: { arg: TableParams }) => {
  14.     return new Promise((resolve) => {
  15.         const { dataSourceId, dbId } = arg;
  16.         if (!dataSourceId || !dbId) return resolve([])
  17.         request(key, { dataSourceId, dbId }).then((res) => resolve(res))
  18.     });
  19. };
复制代码

  • 定义 hooks
  1. const { data: dataSourceList = [], isValidating: isDataSourceFetching } =
  2.     useSWR({ url: "/getDataSource" }, dataSourceFetcher);
  3. const { data: dbList = [], isMutating: isDatabaseFetching, trigger: getDatabase, reset: clearDatabase } = useSWRMutation(
  4.     { url: "/getDatabase" },
  5.     databaseFetcher,
  6. );
  7. const { data: tableList = [], isMutating: isTableFetching, trigger: getTable, reset: clearTable } = useSWRMutation(
  8.     { url: "/getTable" },
  9.     tableFetcher
  10. );
复制代码

  • 手动触发
  1. <Form
  2.     onValuesChange={(changedValue) => {
  3.         if ("dataSourceId" in changedValue) {
  4.             form.resetFields(["dbId", "tableId"]);
  5.             clearDatabase();
  6.             clearTable();
  7.             getDatabase({ dataSourceId: changedValue.dataSourceId });
  8.         }
  9.         if ("dbId" in changedValue) {
  10.             form.resetFields(["tableId"]);
  11.             clearTable();
  12.             getTable({
  13.                 dataSourceId: form.getFieldValue("dataSourceId"),
  14.                 dbId: changedValue.dbId,
  15.             });
  16.         }
  17.     }}
  18. >
  19.     // FormItem略
  20. </Form>
复制代码
13.gif

无缓存写入
14.png

但是使用useSWRMutation这种方式,如果库表还带有远程数据搜索,就没法用到缓存特性了。
性能优化

useSWR 在设计的时候充分考虑了性能问题

  • 自带节流
    当我们短时间内多次调用同一个接口时,只会触发一次请求。如同时渲染多个用户组件,会触发revalidate机制,但实际只会触发一次。这个时间节流时间由dedupingInterval配置控制,默认为2000ms。
A Question, 如何实现防抖呢?无可用配置项

  • revalidate 后freshData与staleData间进行的是深比较,避免不必要渲染, 详见dequal。
  • 依赖收集
    如果没有消费hooks返回的状态,则状态变化不会导致重新渲染
  1. const { data } = useSWR('xxx', fetcher);
  2. // 仅在data变化时render, isValidating, isLoading由于没有引入及时变化也不会触发渲染
复制代码
依赖收集的实现很巧妙

  • 定义个ref进行依赖收集, 默认没有任何依赖
    15.png

  • 通过get实现访问后添加
    16.png

  • 由于state改变必定会导致渲染,所以这些状态全部由useSyncExternalStore管理
    17.png

  • 只有在不相等时才会触发渲染,如果不在stateDependencies收集中,则直接
    18.png

总结

useSWR能够极大的提升用户体验,但在实际使用时,可能仍需留点小心思结合业务来看是否要使用缓存特性,如某些提交业务场景下对库表的实时性很高,这时就该考虑是否要有useSWR了。
再者,数栈产品中在实际开发中很少会对业务数据进行hooks封装,如用户列表可以封装成useUserList,表格使用useList等。感觉更多是开发习惯的原因,觉得以后自己可能也不会复用不会做过多的封装,指令式编程一把梭。
最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
  • 一个针对 antd 的组件测试工具库——ant-design-testing

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册