@tanstack/vue-query
Version:
Hooks for managing, caching and syncing asynchronous and remote data in Vue
344 lines (323 loc) • 12.2 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { QueriesObserver } from '@tanstack/query-core'
import {
computed,
getCurrentScope,
onScopeDispose,
readonly,
ref,
watch,
} from 'vue-demi'
import { useQueryClient } from './useQueryClient'
import { cloneDeepUnref } from './utils'
import type { Ref } from 'vue-demi'
import type {
DefaultError,
DefinedQueryObserverResult,
QueriesObserverOptions,
QueriesPlaceholderDataFunction,
QueryFunction,
QueryKey,
QueryObserverResult,
ThrowOnError,
} from '/query-core'
import type { UseQueryOptions } from './useQuery'
import type { QueryClient } from './queryClient'
import type { DeepUnwrapRef, DistributiveOmit, MaybeRefDeep } from './types'
// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
// `placeholderData` function does not have a parameter
type UseQueryOptionsForUseQueries<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = DistributiveOmit<
UseQueryOptions<TQueryFnData, TError, TData, unknown, TQueryKey>,
'placeholderData'
> & {
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
}
// Avoid TS depth-limit error in case of large array literal
type MAXIMUM_DEPTH = 20
type GetOptions<T> =
// Part 1: if UseQueryOptions are already being sent through, then just return T
T extends UseQueryOptions
? DeepUnwrapRef<T>
: // Part 2: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends {
queryFnData: infer TQueryFnData
error?: infer TError
data: infer TData
}
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseQueryOptionsForUseQueries<unknown, TError, TData>
: // Part 3: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
T extends [infer TQueryFnData, infer TError, infer TData]
? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
: T extends [infer TQueryFnData, infer TError]
? UseQueryOptionsForUseQueries<TQueryFnData, TError>
: T extends [infer TQueryFnData]
? UseQueryOptionsForUseQueries<TQueryFnData>
: // Part 4: responsible for inferring and enforcing type if no explicit parameter was provided
T extends {
queryFn?: QueryFunction<
infer TQueryFnData,
infer TQueryKey
>
select?: (data: any) => infer TData
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? UseQueryOptionsForUseQueries<
TQueryFnData,
TError,
TData,
TQueryKey
>
: T extends {
queryFn?: QueryFunction<
infer TQueryFnData,
infer TQueryKey
>
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? UseQueryOptionsForUseQueries<
TQueryFnData,
TError,
TQueryFnData,
TQueryKey
>
: // Fallback
UseQueryOptionsForUseQueries
// A defined initialData setting should return a DefinedQueryObserverResult rather than QueryObserverResult
type GetDefinedOrUndefinedQueryResult<T, TData, TError = unknown> = T extends {
initialData?: infer TInitialData
}
? unknown extends TInitialData
? QueryObserverResult<TData, TError>
: TInitialData extends TData
? DefinedQueryObserverResult<TData, TError>
: TInitialData extends () => infer TInitialDataResult
? unknown extends TInitialDataResult
? QueryObserverResult<TData, TError>
: TInitialDataResult extends TData
? DefinedQueryObserverResult<TData, TError>
: QueryObserverResult<TData, TError>
: QueryObserverResult<TData, TError>
: QueryObserverResult<TData, TError>
type GetResults<T> =
// Part 1: if using UseQueryOptions then the types are already set
T extends UseQueryOptions<
infer TQueryFnData,
infer TError,
infer TData,
any,
any
>
? GetDefinedOrUndefinedQueryResult<
T,
undefined extends TData ? TQueryFnData : TData,
TError
>
: // Part 2: responsible for mapping explicit type parameter to function result, if object
T extends { queryFnData: any; error?: infer TError; data: infer TData }
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: // Part 3: responsible for mapping explicit type parameter to function result, if tuple
T extends [any, infer TError, infer TData]
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: T extends [infer TQueryFnData, infer TError]
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
: T extends [infer TQueryFnData]
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData>
: // Part 4: responsible for mapping inferred type to results, if no explicit parameter was provided
T extends {
queryFn?: QueryFunction<infer TQueryFnData, any>
select?: (data: any) => infer TData
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? GetDefinedOrUndefinedQueryResult<
T,
unknown extends TData ? TQueryFnData : TData,
unknown extends TError ? DefaultError : TError
>
: T extends {
queryFn?: QueryFunction<infer TQueryFnData, any>
throwOnError?: ThrowOnError<any, infer TError, any, any>
}
? GetDefinedOrUndefinedQueryResult<
T,
TQueryFnData,
unknown extends TError ? DefaultError : TError
>
: // Fallback
QueryObserverResult
/**
* UseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
*/
export type UseQueriesOptions<
T extends Array<any>,
Result extends Array<any> = [],
Depth extends ReadonlyArray<number> = [],
> = Depth['length'] extends MAXIMUM_DEPTH
? Array<UseQueryOptionsForUseQueries>
: T extends []
? []
: T extends [infer Head]
? [...Result, GetOptions<Head>]
: T extends [infer Head, ...infer Tail]
? UseQueriesOptions<
[...Tail],
[...Result, GetOptions<Head>],
[...Depth, 1]
>
: Array<unknown> extends T
? T
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
// use this to infer the param types in the case of Array.map() argument
T extends Array<
UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
infer TQueryKey
>
>
? Array<
UseQueryOptionsForUseQueries<
TQueryFnData,
TError,
TData,
TQueryKey
>
>
: // Fallback
Array<UseQueryOptionsForUseQueries>
/**
* UseQueriesResults reducer recursively maps type param to results
*/
export type UseQueriesResults<
T extends Array<any>,
Result extends Array<any> = [],
Depth extends ReadonlyArray<number> = [],
> = Depth['length'] extends MAXIMUM_DEPTH
? Array<QueryObserverResult>
: T extends []
? []
: T extends [infer Head]
? [...Result, GetResults<Head>]
: T extends [infer Head, ...infer Tail]
? UseQueriesResults<
[...Tail],
[...Result, GetResults<Head>],
[...Depth, 1]
>
: T extends Array<
UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
any
>
>
? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results
Array<
QueryObserverResult<
unknown extends TData ? TQueryFnData : TData,
TError
>
>
: // Fallback
Array<QueryObserverResult>
type UseQueriesOptionsArg<T extends Array<any>> = readonly [
...UseQueriesOptions<T>,
]
export function useQueries<
T extends Array<any>,
TCombinedResult = UseQueriesResults<T>,
>(
{
queries,
...options
}: {
queries: MaybeRefDeep<UseQueriesOptionsArg<T>>
combine?: (result: UseQueriesResults<T>) => TCombinedResult
},
queryClient?: QueryClient,
): Readonly<Ref<TCombinedResult>> {
if (process.env.NODE_ENV === 'development') {
if (!getCurrentScope()) {
console.warn(
'vue-query composables like "useQuery()" should only be used inside a "setup()" function or a running effect scope. They might otherwise lead to memory leaks.',
)
}
}
const client = queryClient || useQueryClient()
const defaultedQueries = computed(() =>
cloneDeepUnref(queries).map((queryOptions) => {
if (typeof queryOptions.enabled === 'function') {
queryOptions.enabled = queryOptions.enabled()
}
const defaulted = client.defaultQueryOptions(queryOptions)
defaulted._optimisticResults = client.isRestoring.value
? 'isRestoring'
: 'optimistic'
return defaulted
}),
)
const observer = new QueriesObserver<TCombinedResult>(
client,
defaultedQueries.value,
options as QueriesObserverOptions<TCombinedResult>,
)
const [, getCombinedResult] = observer.getOptimisticResult(
defaultedQueries.value,
)
const state = ref(getCombinedResult()) as Ref<TCombinedResult>
let unsubscribe = () => {
// noop
}
watch(
client.isRestoring,
(isRestoring) => {
if (!isRestoring) {
unsubscribe()
unsubscribe = observer.subscribe(() => {
const [, getCombinedResultRestoring] = observer.getOptimisticResult(
defaultedQueries.value,
)
state.value = getCombinedResultRestoring()
})
// Subscription would not fire for persisted results
const [, getCombinedResultPersisted] = observer.getOptimisticResult(
defaultedQueries.value,
)
state.value = getCombinedResultPersisted()
}
},
{ immediate: true },
)
watch(
defaultedQueries,
() => {
observer.setQueries(
defaultedQueries.value,
options as QueriesObserverOptions<TCombinedResult>,
)
const [, getCombinedResultPersisted] = observer.getOptimisticResult(
defaultedQueries.value,
)
state.value = getCombinedResultPersisted()
},
{ flush: 'sync' },
)
onScopeDispose(() => {
unsubscribe()
})
return readonly(state) as Readonly<Ref<TCombinedResult>>
}