Inside TanStack Query - network mode
在官方文件提到:TanStack Query 提供三種不同的 network modes,區分沒有網路連線的情況下,Queries, Mutations 的運作方式
TanStack Query provides three different network modes to distinguish how Queries and Mutations should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults.
Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is online.
這裡紀錄 Query
與 network mode 相關實作,參考版本為 v5.45.1
OnlineManager
先從 query-core
的 OnlineManager
開始看起,OnlineManager
是 Subscribable
的子類別
export class OnlineManager extends Subscribable<Listener> {
// ...
constructor() {
super()
// ...
}
// ...
}
Subscribable
大致實作 observer pattern 中的:
listeners
(L4)subscribe
(L12-14)unsubscribe
(L16-19)
Subscriable.onSubscribe()
, Subscriable.onUnsubscribe()
,可以視子類別的需求決定是否實作。因為原型鏈繼承,如果子類別有實作會優先選取 (L26-28, L30-32)
type Listener = () => void
export class Subscribable<TListener extends Function = Listener> {
protected listeners: Set<TListener>
constructor() {
this.listeners = new Set()
this.subscribe = this.subscribe.bind(this)
}
subscribe(listener: TListener): () => void {
this.listeners.add(listener)
this.onSubscribe()
return () => {
this.listeners.delete(listener)
this.onUnsubscribe()
}
}
hasListeners(): boolean {
return this.listeners.size > 0
}
protected onSubscribe(): void {
// Do nothing
}
protected onUnsubscribe(): void {
// Do nothing
}
}
繼續看到 OnlineManager
:
- 預設
#online
狀態為true
(L2) - 建立
OnlineManager
實例時,不會馬上設置 online, offline 的事件監聽 (L9-24),直到有人呼叫OnlineManager.subscribe()
,觸發OnlineManager.onSubscribe()
檢查是否呼叫#setup
設置監聽 (L30-34, L43-47) - 每次收到 online, offline event 會呼叫
setOnline
,當狀態變化時,同時向listeners
進行通知 (L52-57) - TanStack Query 中,只有一個
OnlineManager
實例 (L65)
export class OnlineManager extends Subscribable<Listener> {
#online = true
#cleanup?: () => void
#setup: SetupFn
constructor() {
super()
this.#setup = (onOnline) => {
// addEventListener does not exist in React Native, but window does
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!isServer && window.addEventListener) {
const onlineListener = () => onOnline(true)
const offlineListener = () => onOnline(false)
// Listen to online
window.addEventListener('online', onlineListener, false)
window.addEventListener('offline', offlineListener, false)
return () => {
// Be sure to unsubscribe if a new handler is set
window.removeEventListener('online', onlineListener)
window.removeEventListener('offline', offlineListener)
}
}
return
}
}
protected onSubscribe(): void {
if (!this.#cleanup) {
this.setEventListener(this.#setup)
}
}
protected onUnsubscribe() {
if (!this.hasListeners()) {
this.#cleanup?.()
this.#cleanup = undefined
}
}
setEventListener(setup: SetupFn): void {
this.#setup = setup
this.#cleanup?.()
this.#cleanup = setup(this.setOnline.bind(this))
}
setOnline(online: boolean): void {
const changed = this.#online !== online
if (changed) {
this.#online = online
this.listeners.forEach((listener) => {
listener(online)
})
}
}
isOnline(): boolean {
return this.#online
}
}
export const onlineManager = new OnlineManager()
呼叫 QueryClient.mount()
,QueryClient
會對 OnlineManager
進行訂閱:
export class QueryClient {
// ...
mount(): void {
// ...
this.#unsubscribeOnline = onlineManager.subscribe(async (online) => {
if (online) {
await this.resumePausedMutations()
this.#queryCache.onOnline()
}
})
}
}
tanstack/react-query 是在 QueryClientProvier
透過 useEffect
:
- 設置呼叫
QueryClient.mount()
的 effect callback - 設置呼叫
QueryClient.unmount()
的 cleanup callback
export type QueryClientProviderProps = {
client: QueryClient
children?: React.ReactNode
}
export const QueryClientProvider = ({
client,
children,
}: QueryClientProviderProps): React.JSX.Element => {
React.useEffect(() => {
client.mount()
return () => {
client.unmount()
}
}, [client])
return (
<QueryClientContext.Provider value={client}>
{children}
</QueryClientContext.Provider>
)
}
補充:tanstack/react-query 有 re-export query-core,可以 import OnlineManager
實例,如下方範例:
import { onlineManager } from '@tanstack/react-query'
const unsubscribe = onlineManager.subscribe((isOnline) => {
console.log('isOnline', isOnline)
})
QueryCache.onLine()
在 QueryClient.mount()
得知,當收到 OnlineManager
恢復連線的通知時,會呼叫 this.#queryCache.onOnline()
QueryCache.onOnline
會對所有 Query
呼叫 Query.onOnline()
export class QueryCache extends Subscribable<QueryCacheListener> {
//...
onOnline(): void {
notifyManager.batch(() => {
this.getAll().forEach((query) => {
query.onOnline()
})
})
}
}
Query.onOnline()
先看到前三行 observer
的部分 (observer
為 QueryObserver
的實例,在呼叫 useQuery
後產生,並保存在 Query
中)
- 找到任一需要重新請求的
observer
,QueryObserver.shouldFetchOnReconnect()
會判斷:queryOptions.enabled !== false
queryOptions.refetchOnReconnect === 'always'
,不考慮資料是否過期,直接 refetchqueryOptions.refetchOnReconnect === true
,資料過期才會 refetch
observer?.refetch({ cancelRefetch: false })
:{ cancelRefetch: false }
,表示如果該請求進行中,不做 refetch- 最後會呼叫
this.#currentQuery.fetch()
即為Query.fetch()
export class Query<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Removable {
// ...
onOnline(): void {
const observer = this.observers.find((x) => x.shouldFetchOnReconnect())
observer?.refetch({ cancelRefetch: false })
// Continue fetch if currently paused
this.#retryer?.continue()
}
shouldFetchOnReconnect(): boolean {
return shouldFetchOn(
this.#currentQuery,
this.options,
this.options.refetchOnReconnect,
)
}
}
function shouldFetchOn(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
field: (typeof options)['refetchOnMount'] &
(typeof options)['refetchOnWindowFocus'] &
(typeof options)['refetchOnReconnect'],
) {
if (options.enabled !== false) {
const value = typeof field === 'function' ? field(query) : field
return value === 'always' || (value !== false && isStale(query, options))
}
return false
}
#this.retryer?.continue()
:
- 當
state.fetchStatus === 'paused'
,呼叫retryer.continue()
才會觸發config.onContinue()
執行,這部分細節在下一段說明 config.onContinue()
觸發#dispatch({ type: 'continue' })
,將state.fetchStatus
更新成fetching
(L26-29, L40-44, L49)- 每次透過
Query.dispatch()
更新state
都會向observers
進行通知 (L51-53)
export class Query<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Removable {
// ...
onOnline(): void {
const observer = this.observers.find((x) => x.shouldFetchOnReconnect())
observer?.refetch({ cancelRefetch: false })
// Continue fetch if currently paused
this.#retryer?.continue()
}
// ...
fetch(
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
fetchOptions?: FetchOptions<TQueryFnData>,
): Promise<TData> {
// ...
// Try to fetch the data
this.#retryer = createRetryer({
// ...
onPause: () => {
this.#dispatch({ type: 'pause' })
},
onContinue: () => {
this.#dispatch({ type: 'continue' })
},
networkMode: context.options.networkMode,
canRun: () => true,
})
return this.#retryer.start()
}
#dispatch(action: Action<TData, TError>): void {
const reducer = (
state: QueryState<TData, TError>,
): QueryState<TData, TError> => {
switch (action.type) {
// ...
case 'continue':
return {
...state,
fetchStatus: 'fetching',
}
// ...
}
}
this.state = reducer(this.state)
notifyManager.batch(() => {
this.observers.forEach((observer) => {
observer.onQueryUpdate()
})
this.#cache.notify({ query: this, type: 'updated', action })
})
}
}
看到 Retryer
的實作:
Retryer.start()
內,先透過canStart()
判斷是否暫停請求 (L39-43)canStart()
由catFetch()
及config.canRun()
組成 (L12)canFetch()
參考config.networkMode
及OnlineManager.isOnline()
(L50-52)- 在
Query
設置的config.canRun()
為:() => true
- 呼叫
Retryer.pause()
:- 設置
continueFn
(L18-22) - 觸發在
Query
設置的config.onPause()
, 將Query.state.fetchStatus
更新成paused
(L21) - 後續呼叫
Retryer.continue()
觸發continueFn
執行config.onContinue()
(L32-35)
- 設置
import { onlineManager } from './onlineManager'
export function createRetryer<TData = unknown, TError = DefaultError>(
config: RetryerConfig<TData, TError>,
): Retryer<TData> {
let isResolved = false
let continueFn: ((value?: unknown) => void) | undefined
const canContinue = () =>
focusManager.isFocused() &&
(config.networkMode === 'always' || onlineManager.isOnline()) &&
config.canRun()
const canStart = () => canFetch(config.networkMode) && config.canRun()
const pause = () => {
return new Promise((continueResolve) => {
continueFn = (value) => {
if (isResolved || canContinue()) {
continueResolve(value)
}
}
config.onPause?.()
}).then(() => {
continueFn = undefined
if (!isResolved) {
config.onContinue?.()
}
})
}
return {
// ...
continue: () => {
continueFn?.()
return promise
},
// ...
start: () => {
// Start loop
if (canStart()) {
run()
} else {
pause().then(run)
}
return promise
},
}
}
export function canFetch(networkMode: NetworkMode | undefined): boolean {
return (networkMode ?? 'online') === 'online'
? onlineManager.isOnline()
: true
}
interface RetryerConfig<TData = unknown, TError = DefaultError> {
fn: () => TData | Promise<TData>
initialPromise?: Promise<TData>
abort?: () => void
onError?: (error: TError) => void
onSuccess?: (data: TData) => void
onFail?: (failureCount: number, error: TError) => void
onPause?: () => void
onContinue?: () => void
retry?: RetryValue<TError>
retryDelay?: RetryDelayValue<TError>
networkMode: NetworkMode | undefined
canRun: () => boolean
}
Summary
OnlineManager
繼承Subscriable
,是基於 observer pattern 的實作,可以透過OnlineManager.subscribe()
訂閱目前網路連線狀態OnlineManager
有第一個訂閱者時,才會設置 online, offline 的事件監聽- 呼叫
QueryClinet.mount()
,會向OnlineManager
訂閱網路連線狀態- tanstack/react-query 是在
QueryClientProvier
,透過useEffect
設置呼叫QueryClient.mount()
的 callback,及QueryClient.unmount()
的 cleanup function
- tanstack/react-query 是在
- 當
QueryClient
收到OnlineManager
的恢復連線通知,觸發Query
重新連線的流程:QueryClient
呼叫QueryCache.onOnline()
QueryCache
對所有Query
呼叫Query.onOnline()
Query
根據各自的observers
,options.networkMode
,retryer
,state
,決定是否觸發 refetch
- 預設
networkMode
為online
,預設情況下,資料未過期不觸發 refetch Query
會建立Retryer
實例處理請求,如果networkMode === 'online' && !onlineManager.isOnline()
- 呼叫
Retryer.start()
會先暫停請求 Query.state.fetchStatus
為paused
- 呼叫