Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion packages/vue-query/src/__tests__/queryOptions.test-d.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -362,4 +362,35 @@ describe('queryOptions', () => {

expectTypeOf(options.queryKey).not.toBeUndefined()
})

it('should allow computed queryFn resolving to skipToken', () => {
const id = ref<string | null>('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<string | null>('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>()
})
})
2 changes: 1 addition & 1 deletion packages/vue-query/src/__tests__/useQueries.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
28 changes: 24 additions & 4 deletions packages/vue-query/src/__tests__/useQuery.test-d.ts
Original file line number Diff line number Diff line change
@@ -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 '..'

Expand Down Expand Up @@ -360,13 +360,33 @@ describe('useQuery', () => {
})
})

describe('skipToken', () => {
it('should accept skipToken inside a whole-options getter', () => {
const id = ref<string | null>('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<string>()
}
})
})

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'),
}),
Expand Down
11 changes: 4 additions & 7 deletions packages/vue-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export { QueryCache } from './queryCache'
export { queryOptions } from './queryOptions'
export type {
QueryOptions,
UseQueryOptions,
UndefinedInitialQueryOptions,
DefinedInitialQueryOptions,
UndefinedInitialQueryOptionsWithDataTag,
DefinedInitialQueryOptionsWithDataTag,
} from './queryOptions'
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/vue-query/src/queryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
148 changes: 112 additions & 36 deletions packages/vue-query/src/queryOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { DeepUnwrapRef, MaybeRefOrGetter, ShallowOption } from './types'
import type {
DeepUnwrapRef,
MaybeRef,
MaybeRefDeep,
MaybeRefOrGetter,
ShallowOption,
} from './types'
import type {
DefaultError,
InitialDataFunction,
Expand All @@ -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.
Expand Down Expand Up @@ -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<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> & {
> = 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<TQueryFnData> = {
/**
* 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
Expand All @@ -93,6 +147,36 @@ export type UndefinedInitialQueryOptions<
| NonUndefinedGuard<TQueryFnData>
}

type WithDefinedInitialData<TQueryFnData> = {
/**
* 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<TQueryFnData>
| (() => NonUndefinedGuard<TQueryFnData>)
}

/**
* 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<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
WithUndefinedInitialData<TQueryFnData>

/**
* The options accepted by the `queryOptions` overload selected when `initialData` is set — `data` is never
* `undefined`.
Expand All @@ -107,33 +191,25 @@ export type DefinedInitialQueryOptions<
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = QueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> & {
/**
* 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<TQueryFnData>
| (() => NonUndefinedGuard<TQueryFnData>)
}
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
WithDefinedInitialData<TQueryFnData>

export type UndefinedInitialQueryOptionsWithDataTag<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey> &
> = QueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
WithUndefinedInitialData<TQueryFnData> &
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>

export type DefinedInitialQueryOptionsWithDataTag<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey> &
> = QueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey> &
WithDefinedInitialData<TQueryFnData> &
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>

/**
Expand Down Expand Up @@ -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
* <script setup lang="ts">
* import { queryOptions, skipToken, useQuery } from '@tanstack/vue-query'
Expand Down
2 changes: 1 addition & 1 deletion packages/vue-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
QueryObserverResult,
} from '@tanstack/query-core'
import type { QueryClient } from './queryClient'
import type { UseQueryOptions } from './useQuery'
import type { UseQueryOptions } from './queryOptions'
import type { UseInfiniteQueryOptions } from './useInfiniteQuery'
import type { MaybeRefOrGetter } from './types'

Expand Down
2 changes: 1 addition & 1 deletion packages/vue-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type {
QueryObserverResult,
ThrowOnError,
} from '@tanstack/query-core'
import type { UseQueryOptions } from './useQuery'
import type { UseQueryOptions } from './queryOptions'
import type { QueryClient } from './queryClient'
import type { DeepUnwrapRef, MaybeRefDeep, ShallowOption } from './types'

Expand Down
Loading