@tanstack/query-core
Version:
The framework agnostic core that powers TanStack Query
975 lines (870 loc) • 30.7 kB
text/typescript
import { focusManager } from './focusManager'
import { isServer as isServerEnvironment } from './environmentManager'
import { notifyManager } from './notifyManager'
import { fetchState } from './query'
import { Subscribable } from './subscribable'
import {
isValidTimeout,
noop,
replaceData,
resolveQueryValue,
shallowEqualObjects,
timeUntilStale,
} from './utils'
import { timeoutManager } from './timeoutManager'
import type { ManagedTimerId } from './timeoutManager'
import type { FetchOptions, Query, QueryState } from './query'
import type { QueryClient } from './queryClient'
import type {
DefaultError,
DefaultedQueryObserverOptions,
PlaceholderDataFunction,
QueryKey,
QueryObserverBaseResult,
QueryObserverOptions,
QueryObserverResult,
RefetchOptions,
} from './types'
type QueryObserverListener<TData, TError> = (
result: QueryObserverResult<TData, TError>,
) => void
interface ObserverFetchOptions extends FetchOptions {
throwOnError?: boolean
}
/**
* A `QueryObserver` watches a single query in the `QueryCache` and computes a
* `QueryObserverResult` from its state, recomputing and notifying subscribers
* whenever the underlying query (or the observer's options) changes. It is
* the primitive that framework adapters (e.g. `useQuery`) build their hooks
* on top of, but it can also be used directly to observe and switch between
* queries outside of any framework.
* @example
* ```ts
* const observer = new QueryObserver(queryClient, {
* queryKey: ['posts'],
* queryFn: fetchPosts,
* })
*
* const unsubscribe = observer.subscribe((result) => {
* console.log(result.data)
* })
* ```
*/
export class QueryObserver<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Subscribable<QueryObserverListener<TData, TError>> {
#client: QueryClient
#currentQuery: Query<TQueryFnData, TError, TQueryData, TQueryKey> = undefined!
#currentQueryInitialState: QueryState<TQueryData, TError> = undefined!
#currentResult: QueryObserverResult<TData, TError> = undefined!
#currentResultState?: QueryState<TQueryData, TError>
#currentResultOptions?: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>
#selectError: TError | null
#selectFn?: (data: TQueryData) => TData
#selectResult?: TData
// This property keeps track of the last query with defined data.
// It will be used to pass the previous data and query to the placeholder function between renders.
#lastQueryWithDefinedData?: Query<TQueryFnData, TError, TQueryData, TQueryKey>
#staleTimeoutId?: ManagedTimerId
#refetchIntervalId?: ManagedTimerId
#currentRefetchInterval?: number | false
#trackedProps = new Set<keyof QueryObserverResult>()
constructor(
client: QueryClient,
public options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
) {
super()
this.#client = client
this.#selectError = null
this.bindMethods()
this.setOptions(options)
}
/**
* Binds the methods of the result (`refetch`) to the observer, so they keep working when destructured
* from it. Subclasses override it to bind their own result methods as well.
*/
protected bindMethods(): void {
this.refetch = this.refetch.bind(this)
}
protected override onSubscribe(): void {
if (this.listeners.size === 1) {
this.#currentQuery.addObserver(this)
if (shouldFetchOnMount(this.#currentQuery, this.options)) {
this.#executeFetch()
} else {
this.updateResult()
}
this.#updateTimers()
}
}
protected override onUnsubscribe(): void {
if (!this.hasListeners()) {
this.destroy()
}
}
/**
* Returns whether the observed query is currently stale and configured
* (via the `refetchOnReconnect` option) to refetch when the network
* reconnects.
* @returns `true` if the observer should refetch the query on reconnect.
*/
shouldFetchOnReconnect(): boolean {
return shouldFetchOn(
this.#currentQuery,
this.options,
this.options.refetchOnReconnect,
)
}
/**
* Returns whether the observed query is currently stale and configured
* (via the `refetchOnWindowFocus` option) to refetch when the window
* regains focus.
* @returns `true` if the observer should refetch the query on window focus.
*/
shouldFetchOnWindowFocus(): boolean {
return shouldFetchOn(
this.#currentQuery,
this.options,
this.options.refetchOnWindowFocus,
)
}
/**
* Stops observing the current query: clears all listeners, cancels the
* stale and refetch-interval timers, and removes this observer from the
* query it was observing.
*/
destroy(): void {
this.listeners = new Set()
this.#clearStaleTimeout()
this.#clearRefetchInterval()
this.#currentQuery.removeObserver(this)
}
/**
* Updates the observer's options. This will re-resolve the query being
* observed (switching to a different query if the `queryKey` changed),
* trigger a fetch if the new options require one and the observer has
* subscribers, recompute the current result, and reschedule the stale and
* refetch-interval timers as needed.
* @param options - The new observer options. They are defaulted with {@link
* QueryClient#defaultQueryOptions} before being applied.
* @throws {Error} If `enabled` is neither a boolean nor a function.
* @example
* ```ts
* observer.setOptions({ queryKey: ['posts', 1], queryFn: () => fetchPost(1) })
* // later: switch to a different query, reusing the same observer
* observer.setOptions({ queryKey: ['posts', 2], queryFn: () => fetchPost(2) })
* ```
*/
setOptions(
options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): void {
const prevOptions = this.options
const prevQuery = this.#currentQuery
this.options = this.#client.defaultQueryOptions(options)
if (
this.options.enabled !== undefined &&
typeof this.options.enabled !== 'boolean' &&
typeof this.options.enabled !== 'function' &&
typeof resolveQueryValue(this.options.enabled, this.#currentQuery) !==
'boolean'
) {
throw new Error(
'Expected enabled to be a boolean or a callback that returns a boolean',
)
}
this.#updateQuery()
this.#currentQuery.setOptions(this.options)
if (
prevOptions._defaulted &&
!shallowEqualObjects(this.options, prevOptions)
) {
this.#client.getQueryCache().notify({
type: 'observerOptionsUpdated',
query: this.#currentQuery,
observer: this,
})
}
const mounted = this.hasListeners()
// Fetch if there are subscribers
if (
mounted &&
shouldFetchOptionally(
this.#currentQuery,
prevQuery,
this.options,
prevOptions,
)
) {
this.#executeFetch()
}
// Update result
this.updateResult()
// Update stale interval if needed
if (
mounted &&
(this.#currentQuery !== prevQuery ||
resolveQueryValue(this.options.enabled, this.#currentQuery) !==
resolveQueryValue(prevOptions.enabled, this.#currentQuery) ||
resolveQueryValue(this.options.staleTime, this.#currentQuery) !==
resolveQueryValue(prevOptions.staleTime, this.#currentQuery))
) {
this.#updateStaleTimeout()
}
const nextRefetchInterval = this.#computeRefetchInterval()
// Update refetch interval if needed
if (
mounted &&
(this.#currentQuery !== prevQuery ||
resolveQueryValue(this.options.enabled, this.#currentQuery) !==
resolveQueryValue(prevOptions.enabled, this.#currentQuery) ||
nextRefetchInterval !== this.#currentRefetchInterval)
) {
this.#updateRefetchInterval(nextRefetchInterval)
}
}
/**
* Computes the result the observer would produce for the given (already-defaulted) options
* right now, building the underlying `Query` if it doesn't exist yet, without waiting for a
* subscription callback. Called by framework adapters on every render (e.g. `useQuery`) so the
* returned value is available synchronously, ahead of `setOptions` triggering an actual fetch.
* @param options - The defaulted observer options to compute the result for.
* @returns The result for the given options.
*/
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 (!shallowEqualObjects(this.getCurrentResult(), result)) {
// this assigns the optimistic result to the current Observer
// because if the query function changes, useQuery will be performing
// an effect where it would fetch again.
// When the fetch finishes, we perform a deep data cloning in order
// to reuse objects references. This deep data clone is performed against
// the `observer.currentResult.data` property
// When QueryKey changes, we refresh the query and get new `optimistic`
// result, while we leave the `observer.currentResult`, so when new data
// arrives, it finds the old `observer.currentResult` which is related
// to the old QueryKey. Which means that currentResult and selectData are
// out of sync already.
// To solve this, we move the cursor of the currentResult every time
// an observer reads an optimistic value.
// When keeping the previous data, the result doesn't change until new
// data arrives.
this.#currentResult = result
this.#currentResultOptions = this.options
this.#currentResultState = this.#currentQuery.state
}
return result
}
/**
* Returns the most recently computed `QueryObserverResult` for the
* observed query. This is a point-in-time read; to be notified of updates
* as they happen, subscribe to the observer instead (its inherited
* `subscribe` method).
* @returns The observer's latest result.
* @example
* ```ts
* const result = observer.getCurrentResult()
* console.log(result.status, result.data)
* ```
*/
getCurrentResult(): QueryObserverResult<TData, TError> {
return this.#currentResult
}
/**
* Wraps a `QueryObserverResult` in a `Proxy` that records which properties are read, via
* {@link QueryObserver#trackProp} (and an optional `onPropTracked` callback). Used by framework
* adapters when `notifyOnChangeProps` is not set, to implement its default "only re-render on
* properties you actually read" behavior.
* @param result - The result to wrap.
* @param onPropTracked - Called with the name of each property that is read.
* @returns A proxy of `result` that tracks property reads.
*/
trackResult(
result: QueryObserverResult<TData, TError>,
onPropTracked?: (key: keyof QueryObserverResult) => void,
): QueryObserverResult<TData, TError> {
return new Proxy(result, {
get: (target, key) => {
this.trackProp(key as keyof QueryObserverResult)
onPropTracked?.(key as keyof QueryObserverResult)
return Reflect.get(target, key)
},
})
}
/**
* Records that the given `QueryObserverResult` property was read, so a subsequent update only
* notifies this observer if a tracked property actually changed. Normally called indirectly via
* {@link QueryObserver#trackResult}'s proxy; exposed directly for adapters that track property
* access themselves (e.g. through their own reactivity system) instead of via the proxy.
* @param key - The name of the property that was read.
*/
trackProp(key: keyof QueryObserverResult) {
this.#trackedProps.add(key)
}
/**
* Returns the `Query` instance this observer is currently observing.
* @returns The observed query.
*/
getCurrentQuery(): Query<TQueryFnData, TError, TQueryData, TQueryKey> {
return this.#currentQuery
}
/**
* Refetches the observed query and returns a promise that resolves with
* the resulting `QueryObserverResult`.
* @param options - Set `cancelRefetch` to `false` to keep a running fetch instead of cancelling
* it, and `throwOnError` to `true` to reject when the refetch fails.
* @returns A promise that resolves with the result after the refetch.
* @example
* ```ts
* const result = await observer.refetch({ cancelRefetch: false })
* console.log(result.data)
* ```
*/
refetch({ ...options }: RefetchOptions = {}): Promise<
QueryObserverResult<TData, TError>
> {
return this.fetch({
...options,
})
}
/**
* Fetches a query defined by the given options without affecting this
* observer's own tracked query or result, and returns a promise that
* resolves with the `QueryObserverResult` for that fetch. This is useful
* for prefetching data that another observer (e.g. a query about to be
* navigated to) will need, ahead of time.
* @param options - The observer options of the query to fetch.
* @returns A promise that resolves with the result for the fetched query.
* @example
* ```ts
* const result = await observer.fetchOptimistic({
* queryKey: ['posts', 2],
* queryFn: () => fetchPost(2),
* })
* console.log(result.data)
* ```
*/
fetchOptimistic(
options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): Promise<QueryObserverResult<TData, TError>> {
const defaultedOptions = this.#client.defaultQueryOptions(options)
const query = this.#client
.getQueryCache()
.build(this.#client, defaultedOptions)
let unsubscribe = () => {}
let resolveEarly:
| ((result: QueryObserverResult<TData, TError>) => void)
| undefined
const cachePromise = new Promise<QueryObserverResult<TData, TError>>(
(resolve) => {
resolveEarly = resolve
unsubscribe = this.#client.getQueryCache().subscribe((event) => {
if (
event.type === 'updated' &&
event.query.queryHash === query.queryHash &&
query.state.data !== undefined
) {
unsubscribe()
resolve(this.createResult(query, defaultedOptions))
}
})
},
)
return Promise.race([
query
.fetch(defaultedOptions)
.then(() => {
const result = this.createResult(query, defaultedOptions)
// Settle the subscriber promise so both branches always settle.
// This value is ignored by Promise.race since the fetch branch already won.
resolveEarly?.(result)
return result
})
.finally(() => {
unsubscribe()
}),
cachePromise,
])
}
/**
* Fetches the observed query and updates the result once the fetch settles. Used by `refetch` and,
* in `InfiniteQueryObserver`, to fetch more pages.
* @param fetchOptions - Options for this fetch. `cancelRefetch` defaults to `true`.
* @returns A promise that resolves with the updated result.
*/
protected fetch(
fetchOptions: ObserverFetchOptions,
): Promise<QueryObserverResult<TData, TError>> {
return this.#executeFetch({
...fetchOptions,
cancelRefetch: fetchOptions.cancelRefetch ?? true,
}).then(() => {
this.updateResult()
return this.#currentResult
})
}
#executeFetch(
fetchOptions?: Omit<ObserverFetchOptions, 'initialPromise'>,
): Promise<TQueryData | undefined> {
// Make sure we reference the latest query as the current one might have been removed
this.#updateQuery()
// Fetch
let promise: Promise<TQueryData | undefined> = this.#currentQuery.fetch(
this.options,
fetchOptions,
)
if (!fetchOptions?.throwOnError) {
promise = promise.catch(noop)
}
return promise
}
#shouldScheduleTimer(timeout: unknown): timeout is number {
return (
!isServerEnvironment() &&
resolveQueryValue(this.options.enabled, this.#currentQuery) !== false &&
isValidTimeout(timeout)
)
}
#updateStaleTimeout(): void {
this.#clearStaleTimeout()
const staleTime = resolveQueryValue(
this.options.staleTime,
this.#currentQuery,
)
if (this.#currentResult.isStale || !this.#shouldScheduleTimer(staleTime)) {
return
}
const time = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime)
// The timeout is sometimes triggered 1 ms before the stale time expiration.
// To mitigate this issue we always add 1 ms to the timeout.
const timeout = time + 1
this.#staleTimeoutId = timeoutManager.setTimeout(() => {
if (!this.#currentResult.isStale) {
this.updateResult()
}
}, timeout)
}
#computeRefetchInterval() {
return (
resolveQueryValue(this.options.refetchInterval, this.#currentQuery) ??
false
)
}
#updateRefetchInterval(nextInterval: number | false): void {
this.#clearRefetchInterval()
this.#currentRefetchInterval = nextInterval
if (
this.#currentRefetchInterval === 0 ||
!this.#shouldScheduleTimer(this.#currentRefetchInterval)
) {
return
}
this.#refetchIntervalId = timeoutManager.setInterval(() => {
if (
this.options.refetchIntervalInBackground ||
focusManager.isFocused()
) {
this.#executeFetch()
}
}, this.#currentRefetchInterval)
}
#updateTimers(): void {
this.#updateStaleTimeout()
this.#updateRefetchInterval(this.#computeRefetchInterval())
}
#clearStaleTimeout(): void {
if (this.#staleTimeoutId !== undefined) {
timeoutManager.clearTimeout(this.#staleTimeoutId)
this.#staleTimeoutId = undefined
}
}
#clearRefetchInterval(): void {
if (this.#refetchIntervalId !== undefined) {
timeoutManager.clearInterval(this.#refetchIntervalId)
this.#refetchIntervalId = undefined
}
}
/**
* Computes the result for a query and options from the query's state, applying `select`,
* `placeholderData`, and the derived flags. Subclasses override it to add their own fields.
* @param query - The query to compute the result for.
* @param options - The observer options to compute the result with.
* @returns The computed result.
*/
protected createResult(
query: Query<TQueryFnData, TError, TQueryData, TQueryKey>,
options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): QueryObserverResult<TData, TError> {
const prevQuery = this.#currentQuery
const prevOptions = this.options
const prevResult = this.#currentResult as
| QueryObserverResult<TData, TError>
| undefined
const prevResultState = this.#currentResultState
const prevResultOptions = this.#currentResultOptions
const queryChange = query !== prevQuery
const queryInitialState = queryChange
? query.state
: this.#currentQueryInitialState
const { state } = query
let newState = { ...state }
let isPlaceholderData = false
let data: TData | undefined
// Optimistically set result in fetching state if needed
if (options._optimisticResults) {
const mounted = this.hasListeners()
const fetchOnMount = !mounted && shouldFetchOnMount(query, options)
const fetchOptionally =
mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions)
if (fetchOnMount || fetchOptionally) {
newState = {
...newState,
...fetchState(state.data, query.options),
}
}
if (options._optimisticResults === 'isRestoring') {
newState.fetchStatus = 'idle'
}
}
let { error, errorUpdatedAt, status } = newState
// Per default, use query data
data = newState.data as unknown as TData
let skipSelect = false
// use placeholderData if needed
if (
options.placeholderData !== undefined &&
data === undefined &&
status === 'pending'
) {
let placeholderData
// Memoize placeholder data
if (
prevResult?.isPlaceholderData &&
options.placeholderData === prevResultOptions?.placeholderData
) {
placeholderData = prevResult.data
// we have to skip select when reading this memoization
// because prevResult.data is already "selected"
skipSelect = true
} else {
// compute placeholderData
placeholderData =
typeof options.placeholderData === 'function'
? (
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
)(
this.#lastQueryWithDefinedData?.state.data,
this.#lastQueryWithDefinedData as any,
)
: options.placeholderData
}
if (placeholderData !== undefined) {
status = 'success'
data = replaceData(
prevResult?.data,
placeholderData as unknown,
options,
) as TData
isPlaceholderData = true
}
}
// Select data if needed
// this also runs placeholderData through the select function
if (options.select && data !== undefined && !skipSelect) {
// Memoize select result
if (
prevResult &&
data === prevResultState?.data &&
options.select === this.#selectFn
) {
data = this.#selectResult
} else {
try {
this.#selectFn = options.select
data = options.select(data as any)
data = replaceData(prevResult?.data, data, options)
this.#selectResult = data
this.#selectError = null
} catch (selectError) {
this.#selectError = selectError as TError
}
}
} else if (data === undefined) {
// a stored select error belongs to previously selected data; once that
// data is gone (query switch or reset), it must not leak into this result
this.#selectError = null
}
if (this.#selectError) {
error = this.#selectError
data = this.#selectResult
errorUpdatedAt = Date.now()
status = 'error'
isPlaceholderData = false
}
const isFetching = newState.fetchStatus === 'fetching'
const isPending = status === 'pending'
const isError = status === 'error'
const isLoading = isPending && isFetching
const hasData = data !== undefined
const result: QueryObserverBaseResult<TData, TError> = {
status,
fetchStatus: newState.fetchStatus,
isPending,
isSuccess: status === 'success',
isError,
isInitialLoading: isLoading,
isLoading,
data,
dataUpdatedAt: newState.dataUpdatedAt,
error,
errorUpdatedAt,
failureCount: newState.fetchFailureCount,
failureReason: newState.fetchFailureReason,
errorUpdateCount: newState.errorUpdateCount,
isFetched: query.isFetched(),
isFetchedAfterMount:
newState.dataUpdateCount > queryInitialState.dataUpdateCount ||
newState.errorUpdateCount > queryInitialState.errorUpdateCount,
isFetching,
isRefetching: isFetching && !isPending,
isLoadingError: isError && !hasData,
isPaused: newState.fetchStatus === 'paused',
isPlaceholderData,
isRefetchError: isError && hasData,
isStale: isStale(query, options),
refetch: this.refetch,
isEnabled: resolveQueryValue(options.enabled, query) !== false,
}
const nextResult = result as QueryObserverResult<TData, TError>
return nextResult
}
/**
* Recomputes and stores the current result from the current query/options, notifying listeners
* if it changed. Framework adapters call this right after subscribing to make sure no query
* update was missed in the gap between creating the observer and subscribing to it.
*/
updateResult(): void {
const prevResult = this.#currentResult as
| QueryObserverResult<TData, TError>
| undefined
const nextResult = this.createResult(this.#currentQuery, this.options)
this.#currentResultState = this.#currentQuery.state
this.#currentResultOptions = this.options
if (this.#currentResultState.data !== undefined) {
this.#lastQueryWithDefinedData = this.#currentQuery
}
// Only notify and update result if something has changed
if (shallowEqualObjects(nextResult, prevResult)) {
return
}
this.#currentResult = nextResult
const shouldNotifyListeners = (): boolean => {
if (!prevResult) {
return true
}
const { notifyOnChangeProps } = this.options
const notifyOnChangePropsValue =
typeof notifyOnChangeProps === 'function'
? notifyOnChangeProps()
: notifyOnChangeProps
if (
notifyOnChangePropsValue === 'all' ||
(!notifyOnChangePropsValue && !this.#trackedProps.size)
) {
return true
}
const includedProps = new Set(
notifyOnChangePropsValue ?? this.#trackedProps,
)
if (this.options.throwOnError) {
includedProps.add('error')
}
return Object.keys(this.#currentResult).some((key) => {
const typedKey = key as keyof QueryObserverResult
const changed = this.#currentResult[typedKey] !== prevResult[typedKey]
return changed && includedProps.has(typedKey)
})
}
const notifyListeners = shouldNotifyListeners()
notifyManager.batch(() => {
// First, trigger the listeners
if (notifyListeners) {
this.listeners.forEach((listener) => {
listener(this.#currentResult)
})
}
// Then the cache listeners
this.#client.getQueryCache().notify({
query: this.#currentQuery,
type: 'observerResultsUpdated',
})
})
}
#updateQuery(): void {
const query = this.#client.getQueryCache().build(this.#client, this.options)
if (query === this.#currentQuery) {
return
}
const prevQuery = this.#currentQuery as
| Query<TQueryFnData, TError, TQueryData, TQueryKey>
| undefined
this.#currentQuery = query
this.#currentQueryInitialState = query.state
if (this.hasListeners()) {
prevQuery?.removeObserver(this)
query.addObserver(this)
}
}
/**
* Updates the observer's result when the observed query changes, and reschedules its timers if it
* has listeners.
* @internal
*/
onQueryUpdate(): void {
this.updateResult()
if (this.hasListeners()) {
this.#updateTimers()
}
}
}
/**
* Checks whether a query has no data yet and should do its initial load when an observer mounts.
* @param query - The query to check.
* @param options - The observer options.
* @returns `true` if the query is enabled, has no data, and isn't in an error state with
* `retryOnMount: false`.
*/
function shouldLoadOnMount(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any>,
): boolean {
return (
resolveQueryValue(options.enabled, query) !== false &&
query.state.data === undefined &&
!(
query.state.status === 'error' &&
resolveQueryValue(options.retryOnMount, query) === false
)
)
}
/**
* Checks whether a query should fetch when an observer mounts: either for its initial load, or
* because it has data and `refetchOnMount` asks for a refetch.
* @param query - The query to check.
* @param options - The observer options.
* @returns `true` if the query should fetch on mount.
*/
function shouldFetchOnMount(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
shouldLoadOnMount(query, options) ||
(query.state.data !== undefined &&
shouldFetchOn(query, options, options.refetchOnMount))
)
}
/**
* Checks whether a query should fetch for a `refetchOnMount`, `refetchOnWindowFocus`, or
* `refetchOnReconnect` option.
* @param query - The query to check.
* @param options - The observer options.
* @param field - The value of the option to check.
* @returns `true` if the query is enabled, not static, and the option is `'always'`, or is not
* `false` and the query is stale.
*/
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 (
resolveQueryValue(options.enabled, query) !== false &&
resolveQueryValue(options.staleTime, query) !== 'static'
) {
const value = resolveQueryValue(field, query)
return value === 'always' || (value !== false && isStale(query, options))
}
return false
}
/**
* Checks whether a query should fetch after the observer's options change.
* @param query - The query observed with the new options.
* @param prevQuery - The query observed with the previous options.
* @param options - The new observer options.
* @param prevOptions - The previous observer options.
* @returns `true` if the query changed or was disabled before, it isn't a suspense query in an
* error state, and it is stale.
*/
function shouldFetchOptionally(
query: Query<any, any, any, any>,
prevQuery: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
prevOptions: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
(query !== prevQuery ||
resolveQueryValue(prevOptions.enabled, query) === false) &&
(!options.suspense || query.state.status !== 'error') &&
isStale(query, options)
)
}
/**
* Checks whether a query is stale for the given observer options.
* @param query - The query to check.
* @param options - The observer options, whose `enabled` and `staleTime` are used.
* @returns `true` if the query is enabled and its data is stale for `staleTime`.
*/
function isStale(
query: Query<any, any, any, any>,
options: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
resolveQueryValue(options.enabled, query) !== false &&
query.isStaleByTime(resolveQueryValue(options.staleTime, query))
)
}