@tanstack/query-core
Version:
The framework agnostic core that powers TanStack Query
551 lines (487 loc) • 14 kB
text/typescript
import {
functionalUpdate,
hashKey,
hashQueryKeyByOptions,
noop,
partialMatchKey,
} from './utils'
import { QueryCache } from './queryCache'
import { MutationCache } from './mutationCache'
import { focusManager } from './focusManager'
import { onlineManager } from './onlineManager'
import { notifyManager } from './notifyManager'
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
import type { DataTag, NoInfer } from './types'
import type { QueryState } from './query'
import type {
CancelOptions,
DefaultError,
DefaultOptions,
DefaultedQueryObserverOptions,
FetchInfiniteQueryOptions,
FetchQueryOptions,
InfiniteData,
InvalidateOptions,
InvalidateQueryFilters,
MutationKey,
MutationObserverOptions,
MutationOptions,
QueryClientConfig,
QueryKey,
QueryObserverOptions,
QueryOptions,
RefetchOptions,
RefetchQueryFilters,
ResetOptions,
SetDataOptions,
} from './types'
import type { MutationFilters, QueryFilters, Updater } from './utils'
// TYPES
interface QueryDefaults {
queryKey: QueryKey
defaultOptions: QueryOptions<any, any, any>
}
interface MutationDefaults {
mutationKey: MutationKey
defaultOptions: MutationOptions<any, any, any, any>
}
// CLASS
export class QueryClient {
#queryCache: QueryCache
#mutationCache: MutationCache
#defaultOptions: DefaultOptions
#queryDefaults: Map<string, QueryDefaults>
#mutationDefaults: Map<string, MutationDefaults>
#mountCount: number
#unsubscribeFocus?: () => void
#unsubscribeOnline?: () => void
constructor(config: QueryClientConfig = {}) {
this.#queryCache = config.queryCache || new QueryCache()
this.#mutationCache = config.mutationCache || new MutationCache()
this.#defaultOptions = config.defaultOptions || {}
this.#queryDefaults = new Map()
this.#mutationDefaults = new Map()
this.#mountCount = 0
}
mount(): void {
this.#mountCount++
if (this.#mountCount !== 1) return
this.#unsubscribeFocus = focusManager.subscribe(() => {
if (focusManager.isFocused()) {
this.resumePausedMutations()
this.#queryCache.onFocus()
}
})
this.#unsubscribeOnline = onlineManager.subscribe(() => {
if (onlineManager.isOnline()) {
this.resumePausedMutations()
this.#queryCache.onOnline()
}
})
}
unmount(): void {
this.#mountCount--
if (this.#mountCount !== 0) return
this.#unsubscribeFocus?.()
this.#unsubscribeFocus = undefined
this.#unsubscribeOnline?.()
this.#unsubscribeOnline = undefined
}
isFetching(filters?: QueryFilters): number {
return this.#queryCache.findAll({ ...filters, fetchStatus: 'fetching' })
.length
}
isMutating(filters?: MutationFilters): number {
return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length
}
getQueryData<
TQueryFnData = unknown,
TaggedQueryKey extends QueryKey = QueryKey,
TInferredQueryFnData = TaggedQueryKey extends DataTag<
unknown,
infer TaggedValue
>
? TaggedValue
: TQueryFnData,
>(queryKey: TaggedQueryKey): TInferredQueryFnData | undefined
getQueryData(queryKey: QueryKey) {
return this.#queryCache.find({ queryKey })?.state.data
}
ensureQueryData<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): Promise<TData> {
const cachedData = this.getQueryData<TData>(options.queryKey)
return cachedData !== undefined
? Promise.resolve(cachedData)
: this.fetchQuery(options)
}
getQueriesData<TQueryFnData = unknown>(
filters: QueryFilters,
): Array<[QueryKey, TQueryFnData | undefined]> {
return this.getQueryCache()
.findAll(filters)
.map(({ queryKey, state }) => {
const data = state.data as TQueryFnData | undefined
return [queryKey, data]
})
}
setQueryData<
TQueryFnData = unknown,
TaggedQueryKey extends QueryKey = QueryKey,
TInferredQueryFnData = TaggedQueryKey extends DataTag<
unknown,
infer TaggedValue
>
? TaggedValue
: TQueryFnData,
>(
queryKey: TaggedQueryKey,
updater: Updater<
NoInfer<TInferredQueryFnData> | undefined,
NoInfer<TInferredQueryFnData> | undefined
>,
options?: SetDataOptions,
): TInferredQueryFnData | undefined {
const query = this.#queryCache.find<TInferredQueryFnData>({ queryKey })
const prevData = query?.state.data
const data = functionalUpdate(updater, prevData)
if (typeof data === 'undefined') {
return undefined
}
const defaultedOptions = this.defaultQueryOptions<
any,
any,
unknown,
any,
QueryKey
>({ queryKey })
return this.#queryCache
.build(this, defaultedOptions)
.setData(data, { ...options, manual: true })
}
setQueriesData<TQueryFnData>(
filters: QueryFilters,
updater: Updater<TQueryFnData | undefined, TQueryFnData | undefined>,
options?: SetDataOptions,
): Array<[QueryKey, TQueryFnData | undefined]> {
return notifyManager.batch(() =>
this.getQueryCache()
.findAll(filters)
.map(({ queryKey }) => [
queryKey,
this.setQueryData<TQueryFnData>(queryKey, updater, options),
]),
)
}
getQueryState<TQueryFnData = unknown, TError = DefaultError>(
queryKey: QueryKey,
): QueryState<TQueryFnData, TError> | undefined {
return this.#queryCache.find<TQueryFnData, TError>({ queryKey })?.state
}
removeQueries(filters?: QueryFilters): void {
const queryCache = this.#queryCache
notifyManager.batch(() => {
queryCache.findAll(filters).forEach((query) => {
queryCache.remove(query)
})
})
}
resetQueries(filters?: QueryFilters, options?: ResetOptions): Promise<void> {
const queryCache = this.#queryCache
const refetchFilters: RefetchQueryFilters = {
type: 'active',
...filters,
}
return notifyManager.batch(() => {
queryCache.findAll(filters).forEach((query) => {
query.reset()
})
return this.refetchQueries(refetchFilters, options)
})
}
cancelQueries(
filters: QueryFilters = {},
cancelOptions: CancelOptions = {},
): Promise<void> {
const defaultedCancelOptions = { revert: true, ...cancelOptions }
const promises = notifyManager.batch(() =>
this.#queryCache
.findAll(filters)
.map((query) => query.cancel(defaultedCancelOptions)),
)
return Promise.all(promises).then(noop).catch(noop)
}
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)
})
}
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)
}
fetchQuery<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
>(
options: FetchQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): Promise<TData> {
const defaultedOptions = this.defaultQueryOptions(options)
// https://github.com/tannerlinsley/react-query/issues/652
if (typeof defaultedOptions.retry === 'undefined') {
defaultedOptions.retry = false
}
const query = this.#queryCache.build(this, defaultedOptions)
return query.isStaleByTime(defaultedOptions.staleTime)
? query.fetch(defaultedOptions)
: Promise.resolve(query.state.data as TData)
}
prefetchQuery<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): Promise<void> {
return this.fetchQuery(options).then(noop).catch(noop)
}
fetchInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: FetchInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): Promise<InfiniteData<TData, TPageParam>> {
options.behavior = infiniteQueryBehavior<
TQueryFnData,
TError,
TData,
TPageParam
>(options.pages)
return this.fetchQuery(options)
}
prefetchInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: FetchInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
): Promise<void> {
return this.fetchInfiniteQuery(options).then(noop).catch(noop)
}
resumePausedMutations(): Promise<unknown> {
return this.#mutationCache.resumePausedMutations()
}
getQueryCache(): QueryCache {
return this.#queryCache
}
getMutationCache(): MutationCache {
return this.#mutationCache
}
getDefaultOptions(): DefaultOptions {
return this.#defaultOptions
}
setDefaultOptions(options: DefaultOptions): void {
this.#defaultOptions = options
}
setQueryDefaults(
queryKey: QueryKey,
options: Partial<
Omit<QueryObserverOptions<unknown, any, any, any>, 'queryKey'>
>,
): void {
this.#queryDefaults.set(hashKey(queryKey), {
queryKey,
defaultOptions: options,
})
}
getQueryDefaults(
queryKey: QueryKey,
): QueryObserverOptions<any, any, any, any, any> {
const defaults = [...this.#queryDefaults.values()]
let result: QueryObserverOptions<any, any, any, any, any> = {}
defaults.forEach((queryDefault) => {
if (partialMatchKey(queryKey, queryDefault.queryKey)) {
result = { ...result, ...queryDefault.defaultOptions }
}
})
return result
}
setMutationDefaults(
mutationKey: MutationKey,
options: Omit<MutationObserverOptions<any, any, any, any>, 'mutationKey'>,
): void {
this.#mutationDefaults.set(hashKey(mutationKey), {
mutationKey,
defaultOptions: options,
})
}
getMutationDefaults(
mutationKey: MutationKey,
): MutationObserverOptions<any, any, any, any> {
const defaults = [...this.#mutationDefaults.values()]
let result: MutationObserverOptions<any, any, any, any> = {}
defaults.forEach((queryDefault) => {
if (partialMatchKey(mutationKey, queryDefault.mutationKey)) {
result = { ...result, ...queryDefault.defaultOptions }
}
})
return result
}
defaultQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
>(
options?:
| QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>
| DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
): DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
> {
if (options?._defaulted) {
return options as DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>
}
const defaultedOptions = {
...this.#defaultOptions.queries,
...(options?.queryKey && this.getQueryDefaults(options.queryKey)),
...options,
_defaulted: true,
}
if (!defaultedOptions.queryHash) {
defaultedOptions.queryHash = hashQueryKeyByOptions(
defaultedOptions.queryKey,
defaultedOptions,
)
}
// dependent default values
if (typeof defaultedOptions.refetchOnReconnect === 'undefined') {
defaultedOptions.refetchOnReconnect =
defaultedOptions.networkMode !== 'always'
}
if (typeof defaultedOptions.throwOnError === 'undefined') {
defaultedOptions.throwOnError = !!defaultedOptions.suspense
}
if (
typeof defaultedOptions.networkMode === 'undefined' &&
defaultedOptions.persister
) {
defaultedOptions.networkMode = 'offlineFirst'
}
return defaultedOptions as DefaultedQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>
}
defaultMutationOptions<T extends MutationOptions<any, any, any, any>>(
options?: T,
): T {
if (options?._defaulted) {
return options
}
return {
...this.#defaultOptions.mutations,
...(options?.mutationKey &&
this.getMutationDefaults(options.mutationKey)),
...options,
_defaulted: true,
} as T
}
clear(): void {
this.#queryCache.clear()
this.#mutationCache.clear()
}
}