本文是 React Query 系列第二篇,上一篇,我们概述了 React Query 是什么[1],以及它的 3 个核心概念,包括:
- 查询(Queries)
- 修改(Mutations),和
- 作废缓存(Query Invalidation)
本次,我们将深入第一个核心概念里的内容——useQuery()。
useQuery() 是 React Query 对外提供一个用于封装获取数据请求的包装 React Hook。
import { useQuery } from 'react-query'
function Example() {
const { isLoading, isError, error, data } = useQuery('repoData', () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>
res.json()
)
)
if (isLoading) return 'Loading...'
if (isError) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
如你所见,useQuery 本身并不提供请求能力,而是依赖 Fetch API 或 axios 三方库提供请求能力,useQuery() 做的就是提供响应数据存储和请求状态包装。
接下来,我们就细细来看 useQuery() 的使用
useQuery() API
如果你有看到 useQuery() API 的官方定义[2],就会发现内容非常多。
不过,本文我们先只列最常用的一些。包括:
import { useQuery } from 'react-query'
const {
data,
error,
isError,
isFetching,
isLoading,
isRefetching,
isSuccess,
refetch,
} = useQuery(queryKey, queryFn?, {
enabled,
onError,
onSuccess,
refetchOnWindowFocus,
retry,
select,
staleTime,
})
当然,为了使用 useQuery(),我们还要通过 QueryClientProvider 注入 QueryClient 实例。这部分也是样板代码:
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
import axios from 'axios'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
useQuery() 有赖于 queryClient,这一步是必须的。
接下来,我们要举的案例都基于 <Example> 进行编写。开始吧!
基本案例
先来看一个最简单的例子。
function Example() {
const { isLoading, isError, error, data } = useQuery(
'http200',
() => axios.get('https://httpstat.us/200?sleep=3000')
)
if (isLoading) return 'Loading...'
if (isError) return 'An error has occurred: ' + error.message
return (
<div>
<p>{ JSON.stringify(data.data) }</p>
</div>
)
}
查看效果(3秒 Loading,最后展示数据):
图片
图片
这是使用 useQuery() 的最小可运行 DEMO 了。
错误重试
当然,useQuery() 还能拦截异常,我们将调用 URL 稍稍修改下。
const { isLoading, isError, error, data } = useQuery(
'http500',
() => axios.get('https://httpstat.us/500')
)
查看效果:
图片
你会看到 isLoading 状态会维持一段时间。这是因为,对于错误响应,useQuery() 默认会做重试。
查看控制台:
图片
图片
这里有 4 条一样的请求。useQuery 自带错误重试(Retries)机制,默认 3 次,加上原本的第 1 条,一共 4 条。
如果 4 次都失败了,状态就由 isLoading 变成 isError。
当然,你是可以通过 retry 选项自定义。
useQuery('http200', () => axios.get('https://httpstat.us/500'), { retry: 1 })
再来看看效果:
图片
重试 1 次失败后,就直接报错了。
retry 还支持设置布尔值:true 表示无限重试,false 表示不做任何重试。
缓存数据
另外,在使用 useQuery() 时,我们传入的第一个参数是 Query Key。Query Key 是 React Query 内部用来缓存获取数据的唯一标记,默认会缓存 5 分钟。
那么如何体现呢?我们可以通过 useQuery() 做一次数据获取。
function Example() {
const { isLoading, isError, error, data, refetch } = useQuery(
'http200',
() => axios.get('https://httpstat.us/200?sleep=2000')
)
if (isLoading) return 'Loading...'
if (isError) return 'An error has occurred: ' + error.message
return (
<div>
<p>{ JSON.stringify(data.data) }</p>
<button onClick={refetch}>refetch</button>
</div>
)
}
效果:
图片
会发现,调用 refetch 的过程中,React Query 在控制台强制发起了一个请求。
不过在这个阶段,左侧页面的数据状态没有任何变动——这是由于 React Query 在接受值为 'http200' 的 Query Key 时,发现之前已经缓存过,所以就直接返回了缓存数据。
不过,我们也是有办法监听后台发起的这种请求的状态的,那就是通过 useQuery() 返回的 isFetching 变量。
const { isLoading, isError, error, data, refetch, isFetching } = useQuery('http200', () => axios.get('https://httpstat.us/200?sleep=2000'))
再通过 isFetching 控制按钮状态。
<button disabled={isFetching} notallow={refetch}>{ isFetching ? 'isFeching...' : 'refetch' }</button>
查看效果:
图片
这样,我们就能监听后台默默进行的数据请求了。
当然,数据缓存时间是可以通过 cacheTime 选项自定义的(单位毫秒(ms))。
useQuery('http500', () => axios.get('https://httpstat.us/500'), { cacheTime: 10 * 60 * 1000 })
以上,我们将缓存时间设定成 10 分钟。
默认后台数据的请求行为
useQuery() 还有一个默认行为,非常便捷,就是在网页从后台重新切入或者重新聚焦时,useQuery() 会自动触发后台数据重新获取。
图片
这个机制的原理是,React Query 会监听网页的 visibilitychange 和 focus 事件,当 document.visibilityState 为 'visible' 或者触发 focus 事件时,就会重新请求。
当然,这个行为可以通过 refetchOnWindowFocus 选项禁用掉。
useQuery(
'http200',
() => axios.get('https://httpstat.us/200'),
{ refetchOnWindowFocus: false }
)
注意,从 v5 版本开始,refetchOnWindowFocus 启用时(默认),不再监听 focus 事件[3],避免意外多出来的后台请求[4]的发出。
过期时间
经过以上的学习,你可能会有疑问——既然 React Query 有默认帮我们缓存数据,为什么在缓存数据有效期范围内,还会发起新请求呢?
这就牵扯到另外一个概念,叫过期时间(Stale Time)。
useQuery() 默认获取到的数据,被认为是过期的。虽然重新获取数据时,会利用缓存数据,但只是用于临时展示,新请求获得的数据,会立即替代过期的缓存数据。
当然,这个行为是可以通过 staleTime 选项自定义(默认 staleTime: 0),其类型定义如下:
staleTime: number | Infinity
当为数值时,单位毫秒(ms),表示数据有效/新鲜期。当 staleTime 设置为 Infinity 时,就表示数据永不过期。在缓存有效期范围内,useQuery() 会始终使用缓存数据,而不发起新的请求。
useQuery(
'http200',
() => axios.get('https://httpstat.us/200'),
{ staleTime: Infinity }
)
这样做之后,刷新页面,在获取到一次数据后,不管你如何重新切入/聚焦页面,都没有新的请求发出——这是因为数据在有效期内,另外还有缓存,就直接返回了。
staleTime 与 cacheTime
不过,staleTime 是如何与 cacheTime 配合起作用的呢?
我们再看一个例子:
function Example() {
const { isLoading, isError, error, data, refetch, isFetching } = useQuery('http200', () => axios.get('https://httpstat.us/200?sleep=2000'), { staleTime: Infinity, cacheTime: 5 * 1000})
// ...
}
这里我们设置了数据永不过期,但缓存时间只有 5 秒钟。不过 5 秒钟过后,你重新切入页面,发现还是没有请求。为什么呢?
图片
这是因为“缓存 5 秒钟”,并不是说数据获取后,过 5 秒钟就删掉了!“缓存 5 秒钟”是指当查询缓存在不活跃或未使用,5 秒钟后数据就会被垃圾回收器回收。
那么如何确定“useQuery 查询的缓存是不活跃或未使用”的呢?很简单的一个场景,就是使用 useQuery('http200') 的这个组件被卸载了。
我们改动下 App 组件。
export default function App() {
const [display, setDisplay] = useState(true)
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<>
<button onClick={() => setDisplay(!display)}>{ display ? 'hide' : 'show' }</button>
{ display && <Example /> }
</>
</QueryClientProvider>
)
}
待 <Example /> 数据渲染完成后。
图片
我们再将 <Example /> 销毁,等 5 秒钟再展示<Example /> ——虽然设置了数据永不过期,但缓存时间只有 5 秒钟,现在缓存也没有(被回收了),因此就会发现新请求出来了。
图片
正是因为 cacheTime 这个名称会让人产生歧义,因此从 v5 版本开始,`cacheTime` 选项改名[5]为更符合其含义的 gcTime 了。
总结
本文我们讲解了 useQuery() API 的使用。
首先要知道的是,useQuery 本身并不提供请求能力,而是依赖 Fetch API 或 axios 三方库提供请求能力,useQuery() 做的就是提供响应数据存储和请求状态的包装。
在讲解了 useQuery() 基础使用后,我们介绍了围绕 useQuery() API 的一些核心内容,包括:
- 错误重试(Retries)机制:默认 3 次
- 数据缓存(Query Key):默认 5 分钟
- 过期时间(Stale Time):默认即过期
最后 staleTime 与 cacheTime 之间的区别是:staleTime 决定数据的新鲜度,cacheTime 则用于指定缓存数据。
值得注意的是,“缓存 5 秒钟”并不是说数据获取后,过 5 秒钟就删掉了,而是说查询缓存在不活跃(inactive)或未使用(unused),5 秒钟后数据就被垃圾回收器回收了。
当然限于篇幅,useQuery() 还有内容没讲,这个我们留在下次再说。
好了,希望本文的讲解能够对你有所帮助。感谢阅读,再见。
参考资料
[1]概述了 React Query 是什么: https://juejin.cn/post/7378015213348257855
[2]官方定义: https://tanstack.com/query/v3/docs/framework/react/reference/useQuery
[3]不再监听 focus 事件: https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5#window-focus-refetching-no-longer-listens-to-the-focus-event
[4]意外多出来的后台请求: https://github.com/TanStack/query/pull/4805
[5]cacheTime 选项改名: https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5#rename-cachetime-to-gctime