深入淺出 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
invalidateQueriesmethod 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 的完整探討拉!文章中有任何想進一步了解或內容有誤的地方都歡迎跟我討論。
參考資料
請我喝杯咖啡
如果這裡的內容有幫助到你的話,一杯咖啡就是對我最大的鼓勵。
