前言
本文的定位是 react-query 的进阶教程,我们假定你已经对 react-query 有一定的了解或者熟悉,并能搞清如下问题:
- 了解如何配置 react-query
- 熟悉
useQuery
/useMutation
的使用
如果你连 cache 和 stale 时间都搞不清楚,或者完全对 react-query 小白,可以先阅读入门文章:
《 React Api 请求最佳实践 react-query3 使用教程(比 swr 更好用更强大)》
另外,axios 或者 fetch 的底层封装是属于基础能力,本文不会涉及。
架构设计
useQuery
先聊最重要的 useQuery
,这个东西是查询最常用的。
全局配置最佳实践
首先是全局配置:
import {
QueryClient,
QueryClientProvider
} from 'react-query'
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false
}
}
})
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById('root')
);
熟悉 swr 的都知道,重新聚焦的时候不能再去获取一次数据了,所以我们会配一个 revalidateOnFocus: false
,不然很多情况会造成我只切换下浏览器 tab 你就给我把数据换了,或者当前分页和输入内容等丢了,或者被破坏了,在 react-query 中也是如此,所以聚焦重新 refetch 我们需要关掉。
全局配置最佳实践到此结束。
注意:
-
cacheTime
默认 30s ,其实新鲜时间staleTime
也是一样的,我们一般情况不需要去主动设定,因为我们想要的数据要保证最新性,极少的情况需要控制缓存,所以即使你真的需要缓存配置,请在useQuery
时具体进行个别的配置。 -
可视化 devtools 的配置这里就不谈了,官方文档 写的很清楚,这个东西实际开发中用到的机会还是比较少的,更多情况可以当做一个 当前在开发环境 的小标识 icon。
封装最佳实践
我们遵循 axios / fetch 作为底层封装基础上
=> 进行 api 封装
=> 进行 useQuery hooks 封装
=> 业务中使用
的三步原则。
每个接口建议遵循如下结构:
第一层:api 封装
// apis/index.ts
export interface IGetSearchResultReq {
key: any
}
export interface ISearchResult {
key: any
}
export const getSearchResult = async (params: IGetSearchResultReq) => {
// const res = await fetch('https://www.baidu.com')
const res = {
code: 200,
data: { key: 'value' }
}
return res?.data as ISearchResult | undefined
}
注意:
-
api 封装建议把 type 和函数写在一个文件,为什么要这么做,因为你的 response type 可能很长,如果你单独维护一份
.d.ts
,一是接口多了这个文件会变的特别大,很难阅读,其次是如果以后迁移或者其他项目需要使用,再抽出来会很麻烦,但是你都写在一个文件里,可以保证最小聚合性,作为一个小单元,随时可以移动到任意地方或者其他项目使用。 -
响应直接取
res?.data
即可,但前提是你对异常 code 的处理在 axios 拦截器里做了,所以我们不需要担心 code 的问题,而且我们做了可能undefined
的处理,假如你后续在应用里写的没问题,即使接口炸了应用也不会炸,如果你真的需要根据 code 判断接口成功性,可以把res
整体放出来,这个无所谓。 -
一般 api 接口文件会放在
src/apis/*
或者src/services/*
下,放在哪里并不重要,但是你要清楚。
第二层:hooks 封装
先汇总请求 key:
// apis/key.ts
export const RQ_MODULE_PREFIX = '_RQ_module_name'
export const RQ_SEARCH_RESULT = `${RQ_MODULE_PREFIX}_search_result`
汇总请求 key 的意义在于:
-
在之前的文章介绍过,调取全局 client 进行重复请求或者阻断、获取缓存都是会模糊匹配 key list 的,所以我们是有必要汇总 key 的,为了以后可能的全局操作提供便利性。
-
一个有意义的 key 可以帮助你在 react-query 可视化 devtools 里定位是什么请求。
下一步封装 hooks:
// hooks/useSearchResult.ts
import { useQuery } from 'react-query'
import { isNil, isEmpty } from 'lodash'
import { IGetSearchResultReq, getSearchResult } from '../apis'
import { RQ_SEARCH_RESULT } from '../apis/key'
export const useSearchResult = (params: IGetSearchResultReq) => {
const result = useQuery(
[RQ_SEARCH_RESULT, params],
async () => {
// 你可能有一些其他固定逻辑在这里
// ...
return getSearchResult(params)
},
{
enabled: !isEmpty(params) && !isNil(params?.key),
}
)
return result
}
这里注意几点:
-
这个 case 里的入参是请求参数,有时候你的请求可能是一些固定参数,或者没有 api 请求接口需要的参数那么多,所以根据实际情况写死或者写到这里的 hooks 入参即可,这可以最大限度的帮助你减小入参数量,提高易用性。
-
对于个别的
useQuery
的 config 配置,按实际情况来即可,一般情况我们的入参都是异步的,所以为了保证安全触发性,必须控制什么时候才去查询,使用enabled
选项 + lodash 加持入参判断即可!
这里再说几个常用的 config:
{
// 控制满足什么条件的时候才去请求,保证安全性,一般情况必须指定
enabled: !isEmpty(params) && !isNil(params?.key),
// 每次 新请求开始 到 获取到新数据 前这段时间保留上一次的旧数据
// 一般情况分页查询,表格分页切换这种场景会用到,保证换页时不会出现一个空列表
keepPreviousData: true,
// 轮询间隔,需要轮询的时候使用
refetchInterval: 3e3,
// 获取数据成功时的回调,入参是响应值
// 有时我们只关注这个响应的部分内容,则可以在这个回调里操作,并不需要 useQuery 返回的 data 等
onSuccess: (res) => {
},
// swr 里有个在一定时间内重复请求无效的选项 dedupingInterval
// 在 react-query 里把 cache 和 stale 时间指定一致即可
staleTime: 60 * 1e3,
cacheTime: 60 * 1e3
}
第三层:业务使用
import React from 'react';
import { Spin } from 'antd'
import { useSearchResult } from './hooks/useSearchResult'
function App() {
const {
data: searchResult,
isLoading: searchResultLoading,
isFetching: searchResultFetching,
refetch: searchResultRefetch
} = useSearchResult({
key: 1
})
return (
<Spin spinning={searchResultLoading}>{searchResult?.key}</Spin>
);
}
export default App;
注意:
-
为什么不直接使用结构的
data
还要命一个别名searchResult
,一是为了可读性,二是很多情况你并不只这一个请求,可能是先查一个再拿到结果再去查第二个内容,所以结构后重新命名的意义就存在了。 -
关于
isLoading
和isFetching
的区别就不在提了,在 上篇文章内 已经介绍过,一般情况下,骨架屏使用isLoading
,因为我们不想骨架屏一直在查询时出现,假如是Spin
这种 loading 态,建议使用isFetching
,当然使用isLoading
也并非不可。 -
refetch
是很常用的,比如你保存新数据后重新获取一次最新数据,当然前提是后端不会有短时间的查询强缓存!
另外,假如你有多个查询,为了 loading 的精准性,可以把多个 isFetching
或 isLoading
用 useMemo 写在一起:
const isLoading = useMemo(
() => xxxIsLoaidng || yyyIsLoading,
[xxxIsLoaidng, yyyIsLoading]
)
到此为止,useQuery
的封装部分到此结束。
useMutation
这个 api 我还是不推荐大家经常使用,因为他不强依赖 key 标识,而且也比较麻烦,所以也不推荐封装。
看一个 case:
import React from 'react';
import { message, Button } from 'antd'
import { useMutation } from 'react-query';
function App() {
const mutation = useMutation(async (
// ↓ 注意这里只支持第一个位置入参,多了不支持,如果有多个参数,需要写成对象形式
params: {
key1: any,
key2: any
}
) => {
const res = await requertApi({
// params...
key1,
})
if (res?.code === 200) {
message.success('成功')
return
}
message.error('失败,请重试')
})
return (
<Button
loading={mutation.isLoading}
onClick={() => {
mutation.mutateAsync()
}}
>
按钮
</Button>
);
}
export default App;
乍一看,我们的好处是什么,是 loading 态不需要自己管理了!
可问题是什么:
-
得到的 mutation 必须通过
mutation.mutate()
或mutation.mutateAsync()
调用,不能直接赋值给onClick
,需要多写一步内联函数,否则你就要写一堆useCallback
。 -
mutation 传参只支持第一个参数,也就是只能用第一个位置传参,如果你有多个参数,必须写成对象形式。
总而言之给人的感觉就是麻烦,而且对于使用 useMutation
的 api,都是非查询的 api,所以我们也不需要享受数据一致性的好处,单纯发个请求而已。
所以对于 mutation 这个东西,还是适当使用,适可而止。
总结
可能 useQuery
的上手成本比 swr 稍微高,而且还要封装这么多层是不是太不友好了,要重返 redux + saga 的大量模板代码时代了?
其实不然,封装 hooks 除了减少参数,最重要的是做一些脏活累活,保证你在多处复用时的简便,毕竟在模块化的时代,多处都要查询一个东西是很常见的,所以每个地方都写一次 useQuery
也不友好。
问题在于如何减少重复代码,所以封装的价值也是存在的。
从另一个角度想,假如你有很多项目,都需要使用这个接口,那你直接把封装好的 api 和 hooks 文件拷贝过去就行了。