深入淺出 TanStack Query(三):在呼叫 invalidateQueries 後發生了什麼事
在使用 TanStack Query 時,我們經常需要手動地讓某些或是特定的 query 重新發送請求來取得最新的資料,此時,我們可以使用 invalidateQueries 方法。這篇文章將深入了解在調用 invalidateQueries 後 TanStack Query 做了什麼事,以及 invalidateQueries 與 refetch 的使用比較。
前言
本篇的 TanStack Query 版本為 5.28.7
這是一個跟 TanStack Query 相關的深入原始碼系列文章,TanStack Query 的架構龐大且迭代快速,所以這個系列會不定期更新,下列是目前已經發布的文章:
- 深入淺出 TanStack Query(一):在呼叫 useQuery 後發生了什麼事
- 深入淺出 TanStack Query(二):在呼叫 useMutation 後發生了什麼事
- 深入淺出 TanStack Query(三):在呼叫 invalidateQueries 後發生了什麼事
invalidateQueries 是什麼?
下列是 TanStack Query 官方文件中對於 invalidateQueries
的說明:
The
invalidateQueries
method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as invalid and active queries are refetched in the background.
根據文件我們可以簡單理解 invalidateQueries
是 queryClient
上的一個方法,這個方法可以讓我們依照需求,手動的讓某些或是特定的 query 重新發送請求。像是下列兩種情境就很適合使用 invalidateQueries
。
情境一:當使用 useMutation
新增一筆資料後,我們可能會想要讓 useQuery
重新取得最新的結果,這時我們可以這樣做。
const queryClient = useQueryClient()
const { data } = useQuery({
queryKey: ['TODOS'],
queryFn: fetchTodos
})
const { mutate } = useMutation({
mutationFn: addTodo,
onSuccess() {
return queryClient.invalidateQueries({ queryKey: ['TODOS'] })
}
})
情境二:畫面上有「重新載入」按鈕,當使用者點擊時我們需要取得最新的資料,這時我們可以這樣做。
<script setup lang="ts">
const queryClient = useQueryClient()
const { data } = useQuery({
queryKey: ['TODOS'],
queryFn: fetchTodos
})
const onRefetch = () => {
return queryClient.invalidateQueries({ queryKey: ['TODOS'] })
}
</script>
<template>
<button @click="onRefetch">重新載入</button>
</template>
在呼叫 invalidateQueries
後發生了什麼事
為了一窺究竟,我們來看看 queryClient
上的 invalidateQueries
實作。
class QueryClient {
invalidateQueries(
filters: InvalidateQueryFilters = {},
options: InvalidateOptions = {},
): Promise<void> {
return notifyManager.batch(() => {
this.#queryCache.findAll(filters).forEach((query) => {
query.invalidate()
})
if (filters.refetchType === 'none') {
return Promise.resolve()
}
const refetchFilters: RefetchQueryFilters = {
...filters,
type: filters.refetchType ?? filters.type ?? 'active',
}
return this.refetchQueries(refetchFilters, options)
})
}
}
根據程式碼我們知道在呼叫 invalidateQueries
後,TanStack Query 需要執行下列兩件事情:
- 依照傳入的
filters
找出所有的 query,並呼叫 query 上的invalidate
方法。 - 確認
filters.refetchType
是否為none
,如果是則表示不需要重新發送請求。反之則呼叫refetchQueries
。
接著嘗試更近一步的拆解這兩個步驟。
Query 上的 invalidate
做了什麼事情
要了解 invalidate
做了什麼我們可以到 Query 這個類別中找到他的實作。
class Query extends Removable {
#cache: QueryCache
#observers: Array<QueryObserver<any, any, any, any, any>>
invalidate(): void {
if (!this.state.isInvalidated) {
this.#dispatch({ type: 'invalidate' })
}
}
#dispatch(action: Action<TData, TError>): void {
const reducer = (
state: QueryState<TData, TError>,
): QueryState<TData, TError> => {
switch (action.type) {
// 其他省略
case 'invalidate':
return { ...state, isInvalidated: true }
}
}
this.state = reducer(this.state)
notifyManager.batch(() => {
this.#observers.forEach((observer) => {
observer.onQueryUpdate()
})
this.#cache.notify({ query: this, type: 'updated', action })
})
}
}
雖然程式碼看起來很多,但其實只做了兩件事情:
- 將態上的
isInvalidated
設定為true
。 - 通知所有的 observer 跟 cache 這個 query 已經被更新。
在呼叫 refetchQueries
後發生了什麼事
接著我們來看看 queryClient
上的 refetchQueries
實作。
class QueryClient {
refetchQueries(
filters: RefetchQueryFilters = {},
options?: RefetchOptions,
): Promise<void> {
const fetchOptions = {
...options,
cancelRefetch: options?.cancelRefetch ?? true,
}
const promises = notifyManager.batch(() =>
this.#queryCache
.findAll(filters)
.filter((query) => !query.isDisabled())
.map((query) => {
let promise = query.fetch(undefined, fetchOptions)
if (!fetchOptions.throwOnError) {
promise = promise.catch(noop)
}
return query.state.fetchStatus === 'paused'
? Promise.resolve()
: promise
}),
)
return Promise.all(promises).then(noop)
}
}
一步步說明這段程式碼做了那些事情:
- 依照傳入的
filters
找出所有的 query(這裡找到的會跟invalidateQueries
裡面的一樣)。 - 過濾掉所有
isDisabled()
為true
的 query。 - 調用剩下的 query 上的
fetch
方法重新發送請求。
isDisabled
是一個 query 上的方法,這個方法依照兩件事情來判斷是否為 false
:
class Query extends Removable {
isActive(): boolean {
return this.#observers.some(
(observer) => observer.options.enabled !== false,
)
}
isDisabled(): boolean {
return this.getObserversCount() > 0 && !this.isActive()
}
}
這裡的程式碼邏輯有點繞,簡單列點說明:
- 如果 query 沒有被任何 observer 訂閱則為
false
。 - 如果 query 上的任一個 observer 的
enabled
不為false
則為false
。
經過上述重重的判斷條件
看完了這個段落關於 refetchQueries
的實作分析,結合上一段的內容我們可以完整拼湊出在呼叫 invalidateQueries
後發生了什麼事情。
invalidateQueries vs refetch
在一開始的範例中,我們提到了如果要讓 useQuery
重新取得結果,我們可以使用 invalidateQueries
。但這似乎有點違背直覺,重新取得(refetch)應該比無效(invalidate)更直覺才是,並且 useQuery
也有 refetch
方法可以使用,為什麼需要特地印入 queryClient
的 invalidateQueries
呢?
當然,上面的例子完全可以改使用 refetch
方法。
const { data, refetch } = useQuery({
queryKey: ['TODOS'],
queryFn: fetchTodos
})
const { mutate } = useMutation({
mutationFn: addTodo,
onSuccess() {
return refetch()
}
})
但 TanStack Query 的核心維護成員(@TkDodo)依然推薦使用 invalidateQueries
,更勝於 refetch
,像是這篇討論或是下列這篇推文串。
I mostly agree with Julien. `refetch()` on a disabled observer; `refetch()` after local setState in an event handler. `refetch()` in a useEffect. They all point to underlying issues and a non-idiomatic use of react-query https://t.co/PRLXnNo77L
— Dominik 🔮 (@TkDodo) March 14, 2023
針對這個問題,核心維護成員也進一步提到了兩個理由:
I'd say yes. `refetch` only targets the specific query, while invalidation matches fuzzily. This is important if you have multiple list, e.g. when having filters.
— Dominik 🔮 (@TkDodo) March 14, 2023
Also, most often you don't have access to `refetch` returned from useQuery where your mutation lives
另外,如果我們回顧前面的內容會發現,如果我們要 invalidate
的 query 是禁用狀態的話,TanStack Query 只會把這個 query 標記為無效,不會重新發送請求。這個細節 refetch
就不容易做到,如果選用 refetch
,就算對應到的 query 是禁用狀態也會重新發送請求。這個行為不算是錯誤,這個行為被認定為只是為了繞過 enabled
的一種手段。
結語
在這篇文章中,我們了解了 invalidateQueries
的功能與使用場景,接著深入剖析了在呼叫 invalidateQueries
後發生了什麼事。最後也比較了 invalidateQueries
和 refetch
兩種方法的差異跟官方推薦的使用方式。
深入了解 invalidateQueries
之後,我們發現其整體實作遠比預期的要簡單。除了原始碼外,會特別挑 invalidateQueries
出來寫的另一個原因是在工作上蠻常遇到選用 invalidateQueries
與 refetch
的討論。很顯然地 refetch
不論在使用上跟命名上都比 invalidateQueries
更直覺,如果想要說服團隊選用 invalidateQueries
就需要更完整的論述。
到這裡就是關於 TanStack Query 的 invalidateQueries
的完整探討拉!文章中有任何想進一步了解或內容有誤的地方都歡迎跟我討論。
參考資料
請我喝杯咖啡
如果這裡的內容有幫助到你的話,一杯咖啡就是對我最大的鼓勵。