Inside TanStack Query - stale data

6 min read

紀錄 TanStack Query 判斷 stale data 的機制,包含:

參考版本為 v5.45.1

QueryObserverResult.isStale

const { isStale } = useQuery(...) 開始看起:

useQuery.ts
import { QueryObserver } from '@tanstack/query-core'
 
function useQuery(options: UseQueryOptions, queryClient?: QueryClient) {
  return useBaseQuery(options, QueryObserver, queryClient)
}

呼叫 useQuery()

useBaseQuery.ts
export function useBaseQuery<
  TQueryFnData,
  TError,
  TData,
  TQueryData,
  TQueryKey extends QueryKey,
>(
  options: UseBaseQueryOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryData,
    TQueryKey
  >,
  Observer: typeof QueryObserver,
  queryClient?: QueryClient,
): QueryObserverResult<TData, TError> {
  // ...
  const client = useQueryClient(queryClient)
  const defaultedOptions = client.defaultQueryOptions(options)
  // ...
  const [observer] = React.useState(
    () =>
      new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
        client,
        defaultedOptions,
      ),
  )
 
  const result = observer.getOptimisticResult(defaultedOptions)
  // ...
 
  // Handle result property usage tracking
  return !defaultedOptions.notifyOnChangeProps
    ? observer.trackResult(result)
    : result
}

QueryObserver.getOptimisticResult() 是回傳 QueryObserver.createResult() 的執行結果 (L20, L28)

💡

if (shouldAssignObserverCurrentProperties(this, result)),可以參考:issue#5538, pull#5573, pull#5839


queryObserver.ts
export class QueryObserver<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> extends Subscribable<QueryObserverListener<TData, TError>> {
  // ...
  getOptimisticResult(
    options: DefaultedQueryObserverOptions<
      TQueryFnData,
      TError,
      TData,
      TQueryData,
      TQueryKey
    >,
  ): QueryObserverResult<TData, TError> {
    const query = this.#client.getQueryCache().build(this.#client, options)
 
    const result = this.createResult(query, options)
 
    if (shouldAssignObserverCurrentProperties(this, result)) {
      this.#currentResult = result
      this.#currentResultOptions = this.options
      this.#currentResultState = this.#currentQuery.state
    }
 
    return result
  }
}

QueryObserver.createResult()

queryObserver.ts
export class QueryObserver<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> extends Subscribable<QueryObserverListener<TData, TError>> {
  // ...
  createResult(
    query: Query<TQueryFnData, TError, TQueryData, TQueryKey>,
    options: QueryObserverOptions<
      TQueryFnData,
      TError,
      TData,
      TQueryData,
      TQueryKey
    >,
  ): QueryObserverResult<TData, TError> {
    // ...
    const result: QueryObserverBaseResult<TData, TError> = {
      // ...
      isStale: isStale(query, options),
      // ...
    }
 
    return result as QueryObserverResult<TData, TError>
  }
}
 
function isStale(
  query: Query<any, any, any, any>,
  options: QueryObserverOptions<any, any, any, any, any>,
): boolean {
  return (
    options.enabled !== false &&
    query.isStaleByTime(resolveStaleTime(options.staleTime, query))
  )
}
utils.ts
export function resolveStaleTime<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  staleTime: undefined | StaleTime<TQueryFnData, TError, TData, TQueryKey>,
  query: Query<TQueryFnData, TError, TData, TQueryKey>,
): number | undefined {
  return typeof staleTime === 'function' ? staleTime(query) : staleTime
}
query.ts
import { timeUntilStale } from './utils'
 
export class Query<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> extends Removable {
  // ...
  isStaleByTime(staleTime = 0): boolean {
    return (
      this.state.isInvalidated ||
      this.state.data === undefined ||
      !timeUntilStale(this.state.dataUpdatedAt, staleTime)
    )
  }
  // ...
}
utils
export function timeUntilStale(updatedAt: number, staleTime?: number): number {
  return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0)
}

Query invalidation

QueryClient.invalidateQueries() 根據 filters 參數,找出對應的 Query 呼叫 Query.invalidate()

queryClient.ts
export class QueryClient {
  // ...
  invalidateQueries(
    filters: InvalidateQueryFilters = {},
    options: InvalidateOptions = {},
  ): Promise<void> {
    return notifyManager.batch(() => {
      this.#queryCache.findAll(filters).forEach((query) => {
        query.invalidate()
      })
      // ...
    })
  }
  // ...
}

Query.invalidate()

  1. Query.state.isInvalidated 更新為 true (L12-14, L23-27)
  2. Query 內的 QueryObserver 通知狀態更新 (L35-37)
  3. QueryCache 通知狀態更新 (L39)
query.ts
export class Query<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> extends Removable {
  // ...
  state: QueryState<TData, TError>
  // ...
 
  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 })
    })
  }
}

QueryClient.invalidateQueries() 預設 RefetchQueryFilters.typeactive (L16)

When set to active, only queries that match the refetch predicate and are actively being rendered via useQuery and friends will be refetched in the background.

queryClient.ts
export 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)
    })
  }
  // ...
}

QueryClient.refetchQueries()

先看到 this.#queryCache.findAll(filters),因為有 filters 參數,所以會使用 matchQuery

queryClient.ts
export 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)
  }
}
queryCache.ts
import { matchQuery } from './utils'
 
export class QueryCache extends Subscribable<QueryCacheListener> {
  // ...
  findAll(filters: QueryFilters = {}): Array<Query> {
    const queries = this.getAll()
    return Object.keys(filters).length > 0
      ? queries.filter((query) => matchQuery(filters, query))
      : queries
  }
  // ...
}

上面提到,這裡 filters.type 預設為 active

utils
export function matchQuery(
  filters: QueryFilters,
  query: Query<any, any, any, any>,
): boolean {
  const {
    type = 'all',
    exact,
    fetchStatus,
    predicate,
    queryKey,
    stale,
  } = filters
 
  if (queryKey) {
    if (exact) {
      if (query.queryHash !== hashQueryKeyByOptions(queryKey, query.options)) {
        return false
      }
    } else if (!partialMatchKey(query.queryKey, queryKey)) {
      return false
    }
  }
 
  if (type !== 'all') {
    const isActive = query.isActive()
    if (type === 'active' && !isActive) {
      return false
    }
    if (type === 'inactive' && isActive) {
      return false
    }
  }
  // ...
}
query.ts
export class Query<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> extends Removable {
  // ...
  observers: Array<QueryObserver<any, any, any, any, any>>
  // ...
 
  isActive(): boolean {
    return this.observers.some((observer) => observer.options.enabled !== false)
  }
  // ...
}

接著看回 filter((query) => !query.isDisabled())

queryClient.ts
export 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)
  }
}

Query.isDisabled() 表示:

query.ts
export class Query<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> extends Removable {
  // ...
  observers: Array<QueryObserver<any, any, any, any, any>>
  // ...
 
  isDisabled(): boolean {
    return this.getObserversCount() > 0 && !this.isActive()
  }
  
  getObserversCount(): number {
    return this.observers.length
  }
  // ...
}

Summary

Reference