diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index 8b1ee84c1e..7d16f55b8a 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -1,6 +1,6 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { computed, reactive, ref } from 'vue-demi' -import { dataTagSymbol } from '@tanstack/query-core' +import { dataTagSymbol, skipToken } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { QueryClient } from '../queryClient' import { queryOptions } from '../queryOptions' @@ -362,4 +362,35 @@ describe('queryOptions', () => { expectTypeOf(options.queryKey).not.toBeUndefined() }) + + it('should allow computed queryFn resolving to skipToken', () => { + const id = ref('1') + + const options = queryOptions({ + queryKey: computed(() => ['foo', id.value]), + queryFn: computed(() => + id.value ? () => Promise.resolve({ id: '1' }) : skipToken, + ), + }) + + const { data } = reactive(useQuery(options)) + + expectTypeOf(data).toEqualTypeOf<{ id: string } | undefined>() + }) + + it('should allow skipToken inside a whole-options getter', () => { + const id = ref('1') + + const options = queryOptions(() => { + const current = id.value + return { + queryKey: ['foo', current], + queryFn: current ? () => Promise.resolve({ id: current }) : skipToken, + } + }) + + const { data } = reactive(useQuery(options)) + + expectTypeOf(data).toEqualTypeOf<{ id: string } | undefined>() + }) }) diff --git a/packages/vue-query/src/__tests__/useQueries.test-d.ts b/packages/vue-query/src/__tests__/useQueries.test-d.ts index 93566cb36e..1ee72f4125 100644 --- a/packages/vue-query/src/__tests__/useQueries.test-d.ts +++ b/packages/vue-query/src/__tests__/useQueries.test-d.ts @@ -4,7 +4,7 @@ import { queryKey } from '@tanstack/query-test-utils' import { skipToken, useQueries } from '..' import { queryOptions } from '../queryOptions' import type { OmitKeyof, QueryObserverResult } from '..' -import type { UseQueryOptions } from '../useQuery' +import type { UseQueryOptions } from '../queryOptions' describe('UseQueries config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { diff --git a/packages/vue-query/src/__tests__/useQuery.test-d.ts b/packages/vue-query/src/__tests__/useQuery.test-d.ts index a9b0125ab0..fffedf93b2 100644 --- a/packages/vue-query/src/__tests__/useQuery.test-d.ts +++ b/packages/vue-query/src/__tests__/useQuery.test-d.ts @@ -1,7 +1,7 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { computed, reactive, ref } from 'vue-demi' import { queryKey, sleep } from '@tanstack/query-test-utils' -import { queryOptions, useQuery } from '..' +import { queryOptions, skipToken, useQuery } from '..' import type { Ref } from 'vue-demi' import type { OmitKeyof, UseQueryOptions, UseQueryReturnType } from '..' @@ -360,13 +360,33 @@ describe('useQuery', () => { }) }) + describe('skipToken', () => { + it('should accept skipToken inside a whole-options getter', () => { + const id = ref('1') + + const query = reactive( + useQuery(() => { + const current = id.value + return { + queryKey: ['post', current], + queryFn: current + ? () => sleep(0).then(() => 'Some data') + : skipToken, + } + }), + ) + + if (query.isSuccess) { + expectTypeOf(query.data).toEqualTypeOf() + } + }) + }) + describe('queryKey reactivity rules', () => { - it('should reject a bare reactive getter for the whole queryKey array', () => { + it('should accept a bare reactive getter for the whole queryKey array', () => { const id = ref(1) assertType( useQuery({ - // @ts-expect-error when passed directly to useQuery, queryKey cannot be a bare - // reactive getter for the whole array (queryOptions() allows this) queryKey: () => ['post', id.value], queryFn: () => sleep(0).then(() => 'Some data'), }), diff --git a/packages/vue-query/src/index.ts b/packages/vue-query/src/index.ts index c58b89cf2a..083e1cf138 100644 --- a/packages/vue-query/src/index.ts +++ b/packages/vue-query/src/index.ts @@ -8,6 +8,9 @@ export { QueryCache } from './queryCache' export { queryOptions } from './queryOptions' export type { QueryOptions, + UseQueryOptions, + UndefinedInitialQueryOptions, + DefinedInitialQueryOptions, UndefinedInitialQueryOptionsWithDataTag, DefinedInitialQueryOptionsWithDataTag, } from './queryOptions' @@ -30,13 +33,7 @@ export { VUE_QUERY_CLIENT } from './utils' export type { UsePrefetchQueryOptions } from './usePrefetchQuery' export type { UsePrefetchInfiniteQueryOptions } from './usePrefetchInfiniteQuery' -export type { - UseQueryOptions, - UseQueryReturnType, - UseQueryDefinedReturnType, - UndefinedInitialQueryOptions, - DefinedInitialQueryOptions, -} from './useQuery' +export type { UseQueryReturnType, UseQueryDefinedReturnType } from './useQuery' export type { UseInfiniteQueryOptions, UseInfiniteQueryReturnType, diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts index 8cedfe2756..0e39f93579 100644 --- a/packages/vue-query/src/queryClient.ts +++ b/packages/vue-query/src/queryClient.ts @@ -3,7 +3,7 @@ import { QueryClient as QC } from '@tanstack/query-core' import { cloneDeepUnref } from './utils' import { QueryCache } from './queryCache' import { MutationCache } from './mutationCache' -import type { UseQueryOptions } from './useQuery' +import type { UseQueryOptions } from './queryOptions' import type { Ref } from 'vue-demi' import type { MaybeRefDeep, NoUnknown, QueryClientConfig } from './types' import type { diff --git a/packages/vue-query/src/queryOptions.ts b/packages/vue-query/src/queryOptions.ts index 1b5093298f..21b8b54f9b 100644 --- a/packages/vue-query/src/queryOptions.ts +++ b/packages/vue-query/src/queryOptions.ts @@ -1,4 +1,10 @@ -import type { DeepUnwrapRef, MaybeRefOrGetter, ShallowOption } from './types' +import type { + DeepUnwrapRef, + MaybeRef, + MaybeRefDeep, + MaybeRefOrGetter, + ShallowOption, +} from './types' import type { DefaultError, InitialDataFunction, @@ -10,20 +16,9 @@ import type { } from '@tanstack/query-core' /** - * The options accepted by `queryOptions`, `useQuery`, and the other query hooks. `enabled` tracks reactive - * dependencies automatically as a `ref`, a plain value, or a reactive getter (`() => ...`). `queryKey` reacts - * through a `ref` for the array itself, or `ref`s and reactive getters as individual entries — the array - * itself can't be a bare getter. Other options passed this way are read once and are not reactive. - * - * If you instead pass a getter for the whole options object (`useQuery(() => ({ ... }))`), every option - * inside it — including `staleTime`, `retry`, and `select` — is re-evaluated whenever the getter's own - * reactive dependencies change, since the entire object is recomputed. - * - * `select` only re-runs when `data` changes, or when the `select` function's own reference changes. Since a - * Vue `setup()` function runs only once per component instance, an inline `select` function passed directly - * to `queryOptions`/`useQuery` already has a stable reference across reactive updates. An inline `select` - * created inside a whole-options getter is recreated — and so can change reference — every time that getter - * re-evaluates. + * The plain, unwrapped options that `queryOptions` hands back, and what `useQuery`, `useQueries`, and the + * `queryClient` methods see once `ref`s have been resolved. To pass options in, use + * {@link UseQueryOptions}, which accepts the same options as `ref`s and `computed`s too. * * @template TQueryFnData - The type your `queryFn` resolves to. * @template TError - The type of errors your `queryFn` may throw. @@ -65,21 +60,80 @@ export type QueryOptions< >[Property] } & ShallowOption +// Widen the type of the symbol to enable type inference even if skipToken is not immutable. +type SkipTokenForUseQuery = symbol + /** - * The options accepted by the `queryOptions` overload selected when no `initialData` is set — `data` may be - * `undefined` while the query is `pending`. + * The options accepted by `queryOptions`, `useQuery`, and the other query hooks. `enabled` tracks reactive + * dependencies automatically as a `ref`, a plain value, or a reactive getter (`() => ...`). `queryKey` reacts + * through a `ref` or a reactive getter for the array itself, or `ref`s and reactive getters as individual + * entries. Other options are read once when passed as a plain value, and stay reactive when passed as a `ref` + * or a `computed`. Only `enabled` and `queryKey` read a function as a getter, so `queryFn` reacts through a + * `computed`: a function there is the query function itself. + * + * If you instead pass a getter for the whole options object (`useQuery(() => ({ ... }))`), every option + * inside it — including `staleTime`, `retry`, and `select` — is re-evaluated whenever the getter's own + * reactive dependencies change, since the entire object is recomputed. + * + * `select` only re-runs when `data` changes, or when the `select` function's own reference changes. Since a + * Vue `setup()` function runs only once per component instance, an inline `select` function passed directly + * to `queryOptions`/`useQuery` already has a stable reference across reactive updates. An inline `select` + * created inside a whole-options getter is recreated — and so can change reference — every time that getter + * re-evaluates. * * @template TQueryFnData - The type your `queryFn` resolves to. * @template TError - The type of errors your `queryFn` may throw. * @template TData - The type `data` ends up as after `select` runs. + * @template TQueryData - The type of data stored in the cache, before `select` runs. Defaults to + * `TQueryFnData` and can be configured independently of it. * @template TQueryKey - The type of your `queryKey`. */ -export type UndefinedInitialQueryOptions< +export type UseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, + TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = QueryOptions & { +> = MaybeRef< + { + [Property in keyof QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >]: Property extends 'enabled' | 'queryKey' + ? QueryOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >[Property] + : Property extends 'queryFn' + ? MaybeRefDeep< + | QueryOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >[Property] + | SkipTokenForUseQuery + > + : MaybeRefDeep< + QueryOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >[Property] + > + } & ShallowOption +> + +type WithUndefinedInitialData = { /** * If set, this value will be used as the initial data for the query cache (as long as the query hasn't been * created or cached yet). If set to a function, the function will be called **once** during the shared/root @@ -93,6 +147,36 @@ export type UndefinedInitialQueryOptions< | NonUndefinedGuard } +type WithDefinedInitialData = { + /** + * If set, this value will be used as the initial data for the query cache (as long as the query hasn't been + * created or cached yet). If set to a function, the function will be called **once** during the shared/root + * query initialization, and be expected to synchronously return the initial data. Initial data is + * considered stale by default unless a `staleTime` has been set. `initialData` **is persisted** to the + * cache. Unlike `queryKey`/`enabled`, this is not reactive — it isn't re-evaluated on `ref` changes. + */ + initialData: + | NonUndefinedGuard + | (() => NonUndefinedGuard) +} + +/** + * The options accepted by the `queryOptions` overload selected when no `initialData` is set — `data` may be + * `undefined` while the query is `pending`. + * + * @template TQueryFnData - The type your `queryFn` resolves to. + * @template TError - The type of errors your `queryFn` may throw. + * @template TData - The type `data` ends up as after `select` runs. + * @template TQueryKey - The type of your `queryKey`. + */ +export type UndefinedInitialQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = UseQueryOptions & + WithUndefinedInitialData + /** * The options accepted by the `queryOptions` overload selected when `initialData` is set — `data` is never * `undefined`. @@ -107,25 +191,16 @@ export type DefinedInitialQueryOptions< TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = QueryOptions & { - /** - * If set, this value will be used as the initial data for the query cache (as long as the query hasn't been - * created or cached yet). If set to a function, the function will be called **once** during the shared/root - * query initialization, and be expected to synchronously return the initial data. Initial data is - * considered stale by default unless a `staleTime` has been set. `initialData` **is persisted** to the - * cache. Unlike `queryKey`/`enabled`, this is not reactive — it isn't re-evaluated on `ref` changes. - */ - initialData: - | NonUndefinedGuard - | (() => NonUndefinedGuard) -} +> = UseQueryOptions & + WithDefinedInitialData export type UndefinedInitialQueryOptionsWithDataTag< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = UndefinedInitialQueryOptions & +> = QueryOptions & + WithUndefinedInitialData & QueryKeyWithDataTag export type DefinedInitialQueryOptionsWithDataTag< @@ -133,7 +208,8 @@ export type DefinedInitialQueryOptionsWithDataTag< TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = DefinedInitialQueryOptions & +> = QueryOptions & + WithDefinedInitialData & QueryKeyWithDataTag /** @@ -294,9 +370,9 @@ export function queryOptions< * ``` * * @example - * A parameterized factory that disables the query, type safe, until `postId` is set. This requires a - * whole-options getter: `queryFn` is a single value, not `queryKey`/`enabled`, so it isn't itself reactive — - * the getter is what re-evaluates it on every change to `postId`: + * A parameterized factory that disables the query, type safe, until `postId` is set. The whole-options getter + * re-evaluates `queryFn` on every change to `postId`. `queryFn` can also be a `computed`, but never a bare + * getter, since a function there is the query function itself: * ```vue *