From 9d20f01a78121b1a57c4c9cb5e0a342d4b747740 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sun, 6 Sep 2026 16:03:10 +0900 Subject: [PATCH 01/17] fix(vue-query): widen 'SkipToken' to 'symbol' so it type-checks inside a whole-options getter --- .../src/__tests__/useQuery.test-d.ts | 19 ++++++- packages/vue-query/src/queryOptions.ts | 56 +++++++++++++++---- packages/vue-query/src/useQuery.ts | 29 +++++++--- 3 files changed, 83 insertions(+), 21 deletions(-) diff --git a/packages/vue-query/src/__tests__/useQuery.test-d.ts b/packages/vue-query/src/__tests__/useQuery.test-d.ts index a9b0125ab0..c05bf3da0e 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 '..' @@ -385,4 +385,21 @@ describe('useQuery', () => { expectTypeOf(data.value).toEqualTypeOf() }) }) + + describe('skipToken', () => { + it('should narrow data to string | undefined for a conditional skipToken inside a whole-options getter', () => { + const postId = ref() + + const { data } = useQuery(() => { + const id = postId.value + return { + queryKey: ['post', id], + queryFn: + id != null ? () => sleep(0).then(() => `post ${id}`) : skipToken, + } + }) + + expectTypeOf(data.value).toEqualTypeOf() + }) + }) }) diff --git a/packages/vue-query/src/queryOptions.ts b/packages/vue-query/src/queryOptions.ts index 1b5093298f..972f062cf2 100644 --- a/packages/vue-query/src/queryOptions.ts +++ b/packages/vue-query/src/queryOptions.ts @@ -3,12 +3,18 @@ import type { DefaultError, InitialDataFunction, NonUndefinedGuard, + OmitKeyof, QueryBooleanOption, + QueryFunction, QueryKey, QueryKeyWithDataTag, QueryObserverOptions, } from '@tanstack/query-core' +// Widen `SkipToken`'s `unique symbol` to `symbol` so it survives a `queryFn: cond ? fn : skipToken` +// ternary inside a whole-options getter — see `SkipTokenForUseQueries` in `useQueries.ts`. +type SkipTokenForQueryOptions = symbol + /** * 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 @@ -56,13 +62,18 @@ export type QueryOptions< >) : Property extends 'queryKey' ? MaybeRefOrGetter - : QueryObserverOptions< - TQueryFnData, - TError, - TData, - TQueryData, - DeepUnwrapRef - >[Property] + : Property extends 'queryFn' + ? + | QueryFunction> + | SkipTokenForQueryOptions + | undefined + : QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + DeepUnwrapRef + >[Property] } & ShallowOption /** @@ -120,21 +131,44 @@ export type DefinedInitialQueryOptions< | (() => NonUndefinedGuard) } +// `UndefinedInitialQueryOptions`/`DefinedInitialQueryOptions` widen `queryFn` to plain `symbol` so a +// `queryFn: cond ? fn : skipToken` ternary type-checks as a getter's *input*. Narrow it back to `SkipToken` +// here so the *returned* options object still satisfies `QueryClient` methods that expect `unique symbol`. export type UndefinedInitialQueryOptionsWithDataTag< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = UndefinedInitialQueryOptions & - QueryKeyWithDataTag +> = OmitKeyof< + UndefinedInitialQueryOptions, + 'queryFn' +> & { + queryFn?: QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + DeepUnwrapRef + >['queryFn'] +} & QueryKeyWithDataTag export type DefinedInitialQueryOptionsWithDataTag< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = DefinedInitialQueryOptions & - QueryKeyWithDataTag +> = OmitKeyof< + DefinedInitialQueryOptions, + 'queryFn' +> & { + queryFn?: QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + DeepUnwrapRef + >['queryFn'] +} & QueryKeyWithDataTag /** * You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can diff --git a/packages/vue-query/src/useQuery.ts b/packages/vue-query/src/useQuery.ts index f951c148f5..221de18665 100644 --- a/packages/vue-query/src/useQuery.ts +++ b/packages/vue-query/src/useQuery.ts @@ -6,6 +6,7 @@ import type { InitialDataFunction, NonUndefinedGuard, QueryBooleanOption, + QueryFunction, QueryKey, QueryObserverOptions, } from '@tanstack/query-core' @@ -19,6 +20,10 @@ import type { } from './types' import type { QueryClient } from './queryClient' +// Widen `SkipToken`'s `unique symbol` to `symbol` so it survives a `queryFn: cond ? fn : skipToken` +// ternary inside a whole-options getter — see `SkipTokenForUseQueries` in `useQueries.ts`. +type SkipTokenForUseQuery = symbol + export type UseQueryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -52,15 +57,21 @@ export type UseQueryOptions< TQueryKey >[Property] > - : MaybeRefDeep< - QueryObserverOptions< - TQueryFnData, - TError, - TData, - TQueryData, - DeepUnwrapRef - >[Property] - > + : Property extends 'queryFn' + ? MaybeRefDeep< + | QueryFunction> + | SkipTokenForUseQuery + | undefined + > + : MaybeRefDeep< + QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + DeepUnwrapRef + >[Property] + > } & ShallowOption > From 682dfb53635e5ee97bec625de483625ed78a0072 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sun, 6 Sep 2026 16:03:41 +0900 Subject: [PATCH 02/17] chore(vue-query): add changeset for 'SkipToken' widening fix --- .changeset/vue-query-skiptoken-getter-typecheck.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/vue-query-skiptoken-getter-typecheck.md diff --git a/.changeset/vue-query-skiptoken-getter-typecheck.md b/.changeset/vue-query-skiptoken-getter-typecheck.md new file mode 100644 index 0000000000..17a52297d1 --- /dev/null +++ b/.changeset/vue-query-skiptoken-getter-typecheck.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-query': patch +--- + +fix(vue-query): widen 'SkipToken' to 'symbol' so it type-checks inside a whole-options getter From 940dd0d0a41e9c2d2f6076abe1b3e2c3a42d8140 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Mon, 7 Sep 2026 00:05:38 +0900 Subject: [PATCH 03/17] test(vue-query/useQuery): document that widening 'SkipToken' also accepts unrelated symbols --- .../vue-query/src/__tests__/useQuery.test-d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/vue-query/src/__tests__/useQuery.test-d.ts b/packages/vue-query/src/__tests__/useQuery.test-d.ts index c05bf3da0e..7626a9b1bd 100644 --- a/packages/vue-query/src/__tests__/useQuery.test-d.ts +++ b/packages/vue-query/src/__tests__/useQuery.test-d.ts @@ -401,5 +401,21 @@ describe('useQuery', () => { expectTypeOf(data.value).toEqualTypeOf() }) + + it('known tradeoff: widening SkipToken to a plain symbol also accepts unrelated symbol values', () => { + // `queryFn`'s type accepts any `symbol`, not just `SkipToken`, because narrowing to the `unique + // symbol` that `SkipToken` actually is breaks type inference for the ternary above — same tradeoff + // already accepted in `useQueries.ts`'s `SkipTokenForUseQueries`. This isn't type-safe, but the + // runtime only ever compares `options.queryFn === skipToken` by identity, so passing an unrelated + // symbol here just behaves like `skipToken` at runtime too. + const unrelatedSymbol: unique symbol = Symbol('unrelated') + + const { data } = useQuery({ + queryKey: ['post'], + queryFn: unrelatedSymbol, + }) + + expectTypeOf(data.value).toEqualTypeOf() + }) }) }) From 01691f97653eb8abcb0c32cc22a863fffdf0f719 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Mon, 7 Sep 2026 10:58:43 +0900 Subject: [PATCH 04/17] fix(vue-query): unify 'UseQueryOptions' with 'QueryOptions' so 'queryFn' accepts a 'computed' resolving to 'skipToken' --- .../vue-query-skiptoken-getter-typecheck.md | 2 +- .../__tests__/infiniteQueryOptions.test-d.ts | 26 ++- .../src/__tests__/queryOptions.test-d.ts | 17 +- .../src/__tests__/useQueries.test-d.ts | 2 +- .../src/__tests__/useQueries.test.ts | 32 +++- .../src/__tests__/useQuery.test-d.ts | 4 +- .../vue-query/src/__tests__/useQuery.test.ts | 34 +++- packages/vue-query/src/index.ts | 11 +- packages/vue-query/src/queryClient.ts | 2 +- packages/vue-query/src/queryOptions.ts | 166 +++++++++++------- packages/vue-query/src/useBaseQuery.ts | 2 +- packages/vue-query/src/useQueries.ts | 2 +- packages/vue-query/src/useQuery.ts | 118 ++----------- 13 files changed, 236 insertions(+), 182 deletions(-) diff --git a/.changeset/vue-query-skiptoken-getter-typecheck.md b/.changeset/vue-query-skiptoken-getter-typecheck.md index 17a52297d1..2f9eaa2c70 100644 --- a/.changeset/vue-query-skiptoken-getter-typecheck.md +++ b/.changeset/vue-query-skiptoken-getter-typecheck.md @@ -2,4 +2,4 @@ '@tanstack/vue-query': patch --- -fix(vue-query): widen 'SkipToken' to 'symbol' so it type-checks inside a whole-options getter +fix(vue-query): widen 'SkipToken' to 'symbol' so 'queryFn' type-checks as a 'computed' or inside a whole-options getter, and allow a bare reactive getter for 'queryKey' on 'useQuery'/'useQueries' diff --git a/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts b/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts index 09550310a4..97d3254a8e 100644 --- a/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/infiniteQueryOptions.test-d.ts @@ -1,6 +1,6 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' -import { dataTagSymbol } from '@tanstack/query-core' -import { reactive } from 'vue-demi' +import { dataTagSymbol, skipToken } from '@tanstack/query-core' +import { computed, reactive, ref } from 'vue-demi' import { queryKey } from '@tanstack/query-test-utils' import { infiniteQueryOptions } from '../infiniteQueryOptions' import { QueryClient } from '../queryClient' @@ -162,4 +162,26 @@ describe('infiniteQueryOptions', () => { InfiniteData | undefined >() }) + + it('should allow a computed queryFn resolving to skipToken', () => { + const id = ref('1') + + const options = infiniteQueryOptions({ + queryKey: computed(() => ['foo', id.value]), + queryFn: computed(() => + id.value + ? ({ pageParam }: { pageParam: number }) => + Promise.resolve({ id: id.value, pageParam }) + : skipToken, + ), + getNextPageParam: () => 1, + initialPageParam: 1, + }) + + const { data } = reactive(useInfiniteQuery(options)) + + expectTypeOf(data).toEqualTypeOf< + InfiniteData<{ id: string | null; pageParam: number }> | undefined + >() + }) }) diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index 8b1ee84c1e..21856c25a4 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,19 @@ describe('queryOptions', () => { expectTypeOf(options.queryKey).not.toBeUndefined() }) + + it('should narrow data to a defined type for a 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>() + }) }) 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__/useQueries.test.ts b/packages/vue-query/src/__tests__/useQueries.test.ts index e0ba22953f..9c65a27c71 100644 --- a/packages/vue-query/src/__tests__/useQueries.test.ts +++ b/packages/vue-query/src/__tests__/useQueries.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { onScopeDispose, ref } from 'vue-demi' +import { computed, onScopeDispose, ref } from 'vue-demi' +import { skipToken } from '@tanstack/query-core' import { queryKey, sleep } from '@tanstack/query-test-utils' import { useQueries } from '../useQueries' import { useQueryClient } from '../useQueryClient' @@ -505,4 +506,33 @@ describe('useQueries', () => { expect(fetchFn).toHaveBeenCalledTimes(6) }) + + it('should skip a query while a computed queryFn resolves to skipToken, and run it once defined', async () => { + const key = queryKey() + const id = ref(null) + const fetchFn = vi.fn(() => sleep(10).then(() => 'Some data')) + + const queriesState = useQueries({ + queries: [ + { + queryKey: key, + queryFn: computed(() => (id.value ? fetchFn : skipToken)), + }, + ], + }) + + await vi.advanceTimersByTimeAsync(10) + + expect(fetchFn).not.toHaveBeenCalled() + expect(queriesState.value).toMatchObject([{ status: 'pending' }]) + + id.value = '1' + + await vi.advanceTimersByTimeAsync(10) + + expect(fetchFn).toHaveBeenCalledTimes(1) + expect(queriesState.value).toMatchObject([ + { status: 'success', data: 'Some data' }, + ]) + }) }) diff --git a/packages/vue-query/src/__tests__/useQuery.test-d.ts b/packages/vue-query/src/__tests__/useQuery.test-d.ts index 7626a9b1bd..fd772cf741 100644 --- a/packages/vue-query/src/__tests__/useQuery.test-d.ts +++ b/packages/vue-query/src/__tests__/useQuery.test-d.ts @@ -361,12 +361,10 @@ describe('useQuery', () => { }) 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/__tests__/useQuery.test.ts b/packages/vue-query/src/__tests__/useQuery.test.ts index a648d3b001..f97d97101a 100644 --- a/packages/vue-query/src/__tests__/useQuery.test.ts +++ b/packages/vue-query/src/__tests__/useQuery.test.ts @@ -6,7 +6,11 @@ import { reactive, ref, } from 'vue-demi' -import { QueryObserver, experimental_streamedQuery } from '@tanstack/query-core' +import { + QueryObserver, + experimental_streamedQuery, + skipToken, +} from '@tanstack/query-core' import { queryKey, sleep } from '@tanstack/query-test-utils' import { keepPreviousData } from '..' import { useQuery } from '../useQuery' @@ -778,4 +782,32 @@ describe('useQuery', () => { expect(result.data).toStrictEqual(['chunk1']) }) }) + + describe('skipToken', () => { + it('should skip the query while a computed queryFn resolves to skipToken, and run it once defined', async () => { + const key = queryKey() + const id = ref(null) + const fetchFn = vi.fn(() => sleep(10).then(() => 'Some data')) + + const query = useQuery({ + queryKey: key, + queryFn: computed(() => (id.value ? fetchFn : skipToken)), + }) + + await vi.advanceTimersByTimeAsync(10) + + expect(fetchFn).not.toHaveBeenCalled() + expect(query).toMatchObject({ status: { value: 'pending' } }) + + id.value = '1' + + await vi.advanceTimersByTimeAsync(10) + + expect(fetchFn).toHaveBeenCalledTimes(1) + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: '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 972f062cf2..2b03fdb638 100644 --- a/packages/vue-query/src/queryOptions.ts +++ b/packages/vue-query/src/queryOptions.ts @@ -1,35 +1,30 @@ -import type { DeepUnwrapRef, MaybeRefOrGetter, ShallowOption } from './types' +import type { + DeepUnwrapRef, + MaybeRef, + MaybeRefDeep, + MaybeRefOrGetter, + ShallowOption, +} from './types' import type { DefaultError, InitialDataFunction, NonUndefinedGuard, - OmitKeyof, QueryBooleanOption, - QueryFunction, QueryKey, QueryKeyWithDataTag, QueryObserverOptions, } from '@tanstack/query-core' // Widen `SkipToken`'s `unique symbol` to `symbol` so it survives a `queryFn: cond ? fn : skipToken` -// ternary inside a whole-options getter — see `SkipTokenForUseQueries` in `useQueries.ts`. -type SkipTokenForQueryOptions = symbol +// ternary inside a whole-options getter or a `computed` — see `SkipTokenForUseQueries` in `useQueries.ts`. +// Only `UseQueryOptions` (the *input* type) widens: `QueryOptions` keeps `unique symbol` so the object +// `queryOptions()` hands back still satisfies `QueryClient` methods like `fetchQuery`/`invalidateQueries`. +type SkipTokenForUseQuery = symbol /** - * 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. @@ -62,20 +57,85 @@ export type QueryOptions< >) : Property extends 'queryKey' ? MaybeRefOrGetter - : Property extends 'queryFn' - ? - | QueryFunction> - | SkipTokenForQueryOptions - | undefined - : QueryObserverOptions< - TQueryFnData, - TError, - TData, - TQueryData, - DeepUnwrapRef - >[Property] + : QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + DeepUnwrapRef + >[Property] } & ShallowOption +/** + * 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. `queryFn` reacts through a `ref` or a `computed`, but never a bare getter, since a function there + * is the query function itself. Other options are read once when passed as a plain value, and stay reactive + * when passed as a `ref` or a `computed`. + * + * 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 UseQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = 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 +> + /** * The options accepted by the `queryOptions` overload selected when no `initialData` is set — `data` may be * `undefined` while the query is `pending`. @@ -90,7 +150,7 @@ export type UndefinedInitialQueryOptions< TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = QueryOptions & { +> = UseQueryOptions & { /** * 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 @@ -118,7 +178,7 @@ export type DefinedInitialQueryOptions< TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = QueryOptions & { +> = UseQueryOptions & { /** * 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 @@ -131,25 +191,16 @@ export type DefinedInitialQueryOptions< | (() => NonUndefinedGuard) } -// `UndefinedInitialQueryOptions`/`DefinedInitialQueryOptions` widen `queryFn` to plain `symbol` so a -// `queryFn: cond ? fn : skipToken` ternary type-checks as a getter's *input*. Narrow it back to `SkipToken` -// here so the *returned* options object still satisfies `QueryClient` methods that expect `unique symbol`. export type UndefinedInitialQueryOptionsWithDataTag< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = OmitKeyof< - UndefinedInitialQueryOptions, - 'queryFn' -> & { - queryFn?: QueryObserverOptions< - TQueryFnData, - TError, - TData, - TQueryFnData, - DeepUnwrapRef - >['queryFn'] +> = QueryOptions & { + initialData?: + | undefined + | InitialDataFunction> + | NonUndefinedGuard } & QueryKeyWithDataTag export type DefinedInitialQueryOptionsWithDataTag< @@ -157,17 +208,10 @@ export type DefinedInitialQueryOptionsWithDataTag< TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = OmitKeyof< - DefinedInitialQueryOptions, - 'queryFn' -> & { - queryFn?: QueryObserverOptions< - TQueryFnData, - TError, - TData, - TQueryFnData, - DeepUnwrapRef - >['queryFn'] +> = QueryOptions & { + initialData: + | NonUndefinedGuard + | (() => NonUndefinedGuard) } & QueryKeyWithDataTag /** @@ -328,9 +372,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 *