一個(gè)數(shù)據(jù)獲取竟被 React Query 玩出這么多花樣來(lái)!
本文是 React Query 系列第二篇,上一篇,我們概述了 React Query 是什么[1],以及它的 3 個(gè)核心概念,包括:
- 查詢(Queries)
- 修改(Mutations),和
- 作廢緩存(Query Invalidation)
本次,我們將深入第一個(gè)核心概念里的內(nèi)容——useQuery()。
useQuery() 是 React Query 對(duì)外提供一個(gè)用于封裝獲取數(shù)據(jù)請(qǐng)求的包裝 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 本身并不提供請(qǐng)求能力,而是依賴 Fetch API 或 axios 三方庫(kù)提供請(qǐng)求能力,useQuery() 做的就是提供響應(yīng)數(shù)據(jù)存儲(chǔ)和請(qǐng)求狀態(tài)包裝。
接下來(lái),我們就細(xì)細(xì)來(lái)看 useQuery() 的使用
useQuery() API
如果你有看到 useQuery() API 的官方定義[2],就會(huì)發(fā)現(xiàn)內(nèi)容非常多。
不過(guò),本文我們先只列最常用的一些。包括:
import { useQuery } from 'react-query'
const {
data,
error,
isError,
isFetching,
isLoading,
isRefetching,
isSuccess,
refetch,
} = useQuery(queryKey, queryFn?, {
enabled,
onError,
onSuccess,
refetchOnWindowFocus,
retry,
select,
staleTime,
})
當(dāng)然,為了使用 useQuery(),我們還要通過(guò) QueryClientProvider 注入 QueryClient 實(shí)例。這部分也是樣板代碼:
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,這一步是必須的。
接下來(lái),我們要舉的案例都基于 <Example> 進(jìn)行編寫。開始吧!
基本案例
先來(lái)看一個(gè)最簡(jiǎn)單的例子。
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,最后展示數(shù)據(jù)):
圖片
圖片
這是使用 useQuery() 的最小可運(yùn)行 DEMO 了。
錯(cuò)誤重試
當(dāng)然,useQuery() 還能攔截異常,我們將調(diào)用 URL 稍稍修改下。
const { isLoading, isError, error, data } = useQuery(
'http500',
() => axios.get('https://httpstat.us/500')
)
查看效果:
圖片
你會(huì)看到 isLoading 狀態(tài)會(huì)維持一段時(shí)間。這是因?yàn)?,?duì)于錯(cuò)誤響應(yīng),useQuery() 默認(rèn)會(huì)做重試。
查看控制臺(tái):
圖片
圖片
這里有 4 條一樣的請(qǐng)求。useQuery 自帶錯(cuò)誤重試(Retries)機(jī)制,默認(rèn) 3 次,加上原本的第 1 條,一共 4 條。
如果 4 次都失敗了,狀態(tài)就由 isLoading 變成 isError。
當(dāng)然,你是可以通過(guò) retry 選項(xiàng)自定義。
useQuery('http200', () => axios.get('https://httpstat.us/500'), { retry: 1 })
再來(lái)看看效果:
圖片
重試 1 次失敗后,就直接報(bào)錯(cuò)了。
retry 還支持設(shè)置布爾值:true 表示無(wú)限重試,false 表示不做任何重試。
緩存數(shù)據(jù)
另外,在使用 useQuery() 時(shí),我們傳入的第一個(gè)參數(shù)是 Query Key。Query Key 是 React Query 內(nèi)部用來(lái)緩存獲取數(shù)據(jù)的唯一標(biāo)記,默認(rèn)會(huì)緩存 5 分鐘。
那么如何體現(xiàn)呢?我們可以通過(guò) useQuery() 做一次數(shù)據(jù)獲取。
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>
)
}
效果:
圖片
會(huì)發(fā)現(xiàn),調(diào)用 refetch 的過(guò)程中,React Query 在控制臺(tái)強(qiáng)制發(fā)起了一個(gè)請(qǐng)求。
不過(guò)在這個(gè)階段,左側(cè)頁(yè)面的數(shù)據(jù)狀態(tài)沒有任何變動(dòng)——這是由于 React Query 在接受值為 'http200' 的 Query Key 時(shí),發(fā)現(xiàn)之前已經(jīng)緩存過(guò),所以就直接返回了緩存數(shù)據(jù)。
不過(guò),我們也是有辦法監(jiān)聽后臺(tái)發(fā)起的這種請(qǐng)求的狀態(tài)的,那就是通過(guò) useQuery() 返回的 isFetching 變量。
const { isLoading, isError, error, data, refetch, isFetching } = useQuery('http200', () => axios.get('https://httpstat.us/200?sleep=2000'))
再通過(guò) isFetching 控制按鈕狀態(tài)。
<button disabled={isFetching} notallow={refetch}>{ isFetching ? 'isFeching...' : 'refetch' }</button>
查看效果:
圖片
這樣,我們就能監(jiān)聽后臺(tái)默默進(jìn)行的數(shù)據(jù)請(qǐng)求了。
當(dāng)然,數(shù)據(jù)緩存時(shí)間是可以通過(guò) cacheTime 選項(xiàng)自定義的(單位毫秒(ms))。
useQuery('http500', () => axios.get('https://httpstat.us/500'), { cacheTime: 10 * 60 * 1000 })
以上,我們將緩存時(shí)間設(shè)定成 10 分鐘。
默認(rèn)后臺(tái)數(shù)據(jù)的請(qǐng)求行為
useQuery() 還有一個(gè)默認(rèn)行為,非常便捷,就是在網(wǎng)頁(yè)從后臺(tái)重新切入或者重新聚焦時(shí),useQuery() 會(huì)自動(dòng)觸發(fā)后臺(tái)數(shù)據(jù)重新獲取。
圖片
這個(gè)機(jī)制的原理是,React Query 會(huì)監(jiān)聽網(wǎng)頁(yè)的 visibilitychange 和 focus 事件,當(dāng) document.visibilityState 為 'visible' 或者觸發(fā) focus 事件時(shí),就會(huì)重新請(qǐng)求。
當(dāng)然,這個(gè)行為可以通過(guò) refetchOnWindowFocus 選項(xiàng)禁用掉。
useQuery(
'http200',
() => axios.get('https://httpstat.us/200'),
{ refetchOnWindowFocus: false }
)
注意,從 v5 版本開始,refetchOnWindowFocus 啟用時(shí)(默認(rèn)),不再監(jiān)聽 focus 事件[3],避免意外多出來(lái)的后臺(tái)請(qǐng)求[4]的發(fā)出。
過(guò)期時(shí)間
經(jīng)過(guò)以上的學(xué)習(xí),你可能會(huì)有疑問(wèn)——既然 React Query 有默認(rèn)幫我們緩存數(shù)據(jù),為什么在緩存數(shù)據(jù)有效期范圍內(nèi),還會(huì)發(fā)起新請(qǐng)求呢?
這就牽扯到另外一個(gè)概念,叫過(guò)期時(shí)間(Stale Time)。
useQuery() 默認(rèn)獲取到的數(shù)據(jù),被認(rèn)為是過(guò)期的。雖然重新獲取數(shù)據(jù)時(shí),會(huì)利用緩存數(shù)據(jù),但只是用于臨時(shí)展示,新請(qǐng)求獲得的數(shù)據(jù),會(huì)立即替代過(guò)期的緩存數(shù)據(jù)。
當(dāng)然,這個(gè)行為是可以通過(guò) staleTime 選項(xiàng)自定義(默認(rèn) staleTime: 0),其類型定義如下:
staleTime: number | Infinity
當(dāng)為數(shù)值時(shí),單位毫秒(ms),表示數(shù)據(jù)有效/新鮮期。當(dāng) staleTime 設(shè)置為 Infinity 時(shí),就表示數(shù)據(jù)永不過(guò)期。在緩存有效期范圍內(nèi),useQuery() 會(huì)始終使用緩存數(shù)據(jù),而不發(fā)起新的請(qǐng)求。
useQuery(
'http200',
() => axios.get('https://httpstat.us/200'),
{ staleTime: Infinity }
)
這樣做之后,刷新頁(yè)面,在獲取到一次數(shù)據(jù)后,不管你如何重新切入/聚焦頁(yè)面,都沒有新的請(qǐng)求發(fā)出——這是因?yàn)閿?shù)據(jù)在有效期內(nèi),另外還有緩存,就直接返回了。
staleTime 與 cacheTime
不過(guò),staleTime 是如何與 cacheTime 配合起作用的呢?
我們?cè)倏匆粋€(gè)例子:
function Example() {
const { isLoading, isError, error, data, refetch, isFetching } = useQuery('http200', () => axios.get('https://httpstat.us/200?sleep=2000'), { staleTime: Infinity, cacheTime: 5 * 1000})
// ...
}
這里我們?cè)O(shè)置了數(shù)據(jù)永不過(guò)期,但緩存時(shí)間只有 5 秒鐘。不過(guò) 5 秒鐘過(guò)后,你重新切入頁(yè)面,發(fā)現(xiàn)還是沒有請(qǐng)求。為什么呢?
圖片
這是因?yàn)椤熬彺?5 秒鐘”,并不是說(shuō)數(shù)據(jù)獲取后,過(guò) 5 秒鐘就刪掉了!“緩存 5 秒鐘”是指當(dāng)查詢緩存在不活躍或未使用,5 秒鐘后數(shù)據(jù)就會(huì)被垃圾回收器回收。
那么如何確定“useQuery 查詢的緩存是不活躍或未使用”的呢?很簡(jiǎn)單的一個(gè)場(chǎng)景,就是使用 useQuery('http200') 的這個(gè)組件被卸載了。
我們改動(dòng)下 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 /> 數(shù)據(jù)渲染完成后。
圖片
我們?cè)賹?nbsp;<Example /> 銷毀,等 5 秒鐘再展示<Example /> ——雖然設(shè)置了數(shù)據(jù)永不過(guò)期,但緩存時(shí)間只有 5 秒鐘,現(xiàn)在緩存也沒有(被回收了),因此就會(huì)發(fā)現(xiàn)新請(qǐng)求出來(lái)了。
圖片
正是因?yàn)?cacheTime 這個(gè)名稱會(huì)讓人產(chǎn)生歧義,因此從 v5 版本開始,`cacheTime` 選項(xiàng)改名[5]為更符合其含義的 gcTime 了。
總結(jié)
本文我們講解了 useQuery() API 的使用。
首先要知道的是,useQuery 本身并不提供請(qǐng)求能力,而是依賴 Fetch API 或 axios 三方庫(kù)提供請(qǐng)求能力,useQuery() 做的就是提供響應(yīng)數(shù)據(jù)存儲(chǔ)和請(qǐng)求狀態(tài)的包裝。
在講解了 useQuery() 基礎(chǔ)使用后,我們介紹了圍繞 useQuery() API 的一些核心內(nèi)容,包括:
- 錯(cuò)誤重試(Retries)機(jī)制:默認(rèn) 3 次
- 數(shù)據(jù)緩存(Query Key):默認(rèn) 5 分鐘
- 過(guò)期時(shí)間(Stale Time):默認(rèn)即過(guò)期
最后 staleTime 與 cacheTime 之間的區(qū)別是:staleTime 決定數(shù)據(jù)的新鮮度,cacheTime 則用于指定緩存數(shù)據(jù)。
值得注意的是,“緩存 5 秒鐘”并不是說(shuō)數(shù)據(jù)獲取后,過(guò) 5 秒鐘就刪掉了,而是說(shuō)查詢緩存在不活躍(inactive)或未使用(unused),5 秒鐘后數(shù)據(jù)就被垃圾回收器回收了。
當(dāng)然限于篇幅,useQuery() 還有內(nèi)容沒講,這個(gè)我們留在下次再說(shuō)。
好了,希望本文的講解能夠?qū)δ阌兴鶐椭?。感謝閱讀,再見。
參考資料
[1]概述了 React Query 是什么: https://juejin.cn/post/7378015213348257855
[2]官方定義: https://tanstack.com/query/v3/docs/framework/react/reference/useQuery
[3]不再監(jiān)聽 focus 事件: https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5#window-focus-refetching-no-longer-listens-to-the-focus-event
[4]意外多出來(lái)的后臺(tái)請(qǐng)求: https://github.com/TanStack/query/pull/4805
[5]cacheTime 選項(xiàng)改名: https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5#rename-cachetime-to-gctime