Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/vue-query-enabled-query-callback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-query': patch
---

fix(vue-query): keep query-core's `enabled(query)` callback. Zero-argument getters and refs stay reactive; the query callback is passed through to query-core.
23 changes: 23 additions & 0 deletions packages/vue-query/src/__tests__/queryOptions.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,29 @@ describe('queryOptions', () => {
expectTypeOf(options.queryKey).not.toBeUndefined()
})

it('should allow a getter that returns boolean or undefined', () => {
const someRef = ref<boolean | undefined>(false)

const options = queryOptions({
queryKey: createKey(),
queryFn: () => Promise.resolve(1),
enabled: () => someRef.value,
})

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

it('should type enabled with the query argument from query-core', () => {
queryOptions({
queryKey: ['post'] as const,
queryFn: () => Promise.resolve(1),
enabled: (query) => {
expectTypeOf(query.queryKey).toEqualTypeOf<readonly ['post']>()
return query.state.status !== 'pending'
},
})
})

it('should allow computed ref as queryKey', () => {
const id = ref<string | null>('1')

Expand Down
49 changes: 49 additions & 0 deletions packages/vue-query/src/__tests__/useQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,27 @@ describe('useQuery', () => {
)
})

it('should pass the query to an enabled callback instead of calling it as a getter', () => {
const key = createKey()
const queryFn = vi.fn<() => string>(() => 'foo')
const enabled = vi.fn<
(query: { queryKey: ReadonlyArray<unknown> }) => boolean
>((query) => Array.isArray(query.queryKey))

useQuery({
queryKey: key,
queryFn,
enabled,
})

expect(queryFn).toHaveBeenCalledTimes(1)
expect(enabled.mock.calls.length).toBeGreaterThan(0)
for (const call of enabled.mock.calls) {
expect(call).toHaveLength(1)
expect(call[0]).toStrictEqual(expect.objectContaining({ queryKey: key }))
}
})

it('should allow getters for query keys', async () => {
const key = createKey()
const queryFn = vi.fn<() => string>(() => 'foo')
Expand Down Expand Up @@ -724,6 +745,34 @@ describe('useQuery', () => {
)
})

it('should keep suspense pending while an enabled callback returns false', async () => {
const key = createKey()
const phase = ref('off')
const queryFn = vi.fn<() => Promise<string>>(() =>
sleep(10).then(() => 'Some data'),
)
const onResolve =
vi.fn<(result: QueryObserverResult<string, Error>) => void>()
const query = useQuery({
queryKey: [...key, phase],
queryFn,
enabled: (currentQuery) => currentQuery.queryKey.at(-1) === 'on',
})

query.suspense().then(onResolve)
await vi.advanceTimersByTimeAsync(10)
expect(queryFn).not.toHaveBeenCalled()
expect(onResolve).not.toHaveBeenCalled()

phase.value = 'on'
await vi.advanceTimersByTimeAsync(10)
expect(queryFn).toHaveBeenCalledTimes(1)
expect(onResolve).toHaveBeenCalledTimes(1)
expect(onResolve).toHaveBeenCalledWith(
expect.objectContaining({ data: 'Some data' }),
)
})

it('should resolve immediately without refetching when the data is fresh', () => {
const key = createKey()
const queryFn = vi.fn<() => Promise<string>>(() =>
Expand Down
1 change: 1 addition & 0 deletions packages/vue-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { QueryCache } from './queryCache'
export { queryOptions } from './queryOptions'
export type {
QueryOptions,
VueQueryEnabledOption,
UseQueryOptions,
UndefinedInitialQueryOptions,
DefinedInitialQueryOptions,
Expand Down
32 changes: 23 additions & 9 deletions packages/vue-query/src/queryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ import type {
QueryObserverOptions,
} from '@tanstack/query-core'

/**
* `enabled` for Vue Query. A `ref`, `computed`, or plain boolean is reactive. A zero-argument getter
* (`() => ...`) is too: it is called while tracking dependencies. query-core's `(query) => boolean` is
* kept as that callback and is not tracked by Vue — a `ref` read inside it is current when core calls it,
* and a `ref` change does not by itself call it again.
*/
export type VueQueryEnabledOption<
TQueryFnData,
TError,
TQueryData,
TQueryKey extends QueryKey,
> =
| MaybeRefOrGetter<boolean | undefined>
| QueryBooleanOption<TQueryFnData, TError, TQueryData, TQueryKey>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/**
* 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
Expand Down Expand Up @@ -42,14 +57,12 @@ export type QueryOptions<
TQueryKey
>
]: Property extends 'enabled'
?
| MaybeRefOrGetter<boolean | undefined>
| (() => QueryBooleanOption<
TQueryFnData,
TError,
TQueryData,
DeepUnwrapRef<TQueryKey>
>)
? VueQueryEnabledOption<
TQueryFnData,
TError,
TQueryData,
DeepUnwrapRef<TQueryKey>
>
: Property extends 'queryKey'
? MaybeRefOrGetter<TQueryKey>
: QueryObserverOptions<
Expand All @@ -66,7 +79,8 @@ 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
* dependencies as a `ref`, a plain boolean, or a zero-argument getter (`() => ...`). query-core's
* `(query) => boolean` callback is accepted too and is passed through untracked. `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
Expand Down
19 changes: 15 additions & 4 deletions packages/vue-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ export function useBaseQuery<
}
const clonedOptions = cloneDeepUnref(resolvedOptions as any)

if (typeof clonedOptions.enabled === 'function') {
// A zero-arg function is a Vue getter and must run here so its refs are tracked.
// `(query) => boolean` is query-core's callback and is left for core to call.
if (
typeof clonedOptions.enabled === 'function' &&
clonedOptions.enabled.length === 0
) {
Comment thread
DamianOsipiuk marked this conversation as resolved.
Resolved
clonedOptions.enabled = clonedOptions.enabled()
}

Expand Down Expand Up @@ -176,9 +181,15 @@ export function useBaseQuery<
// noop
}
const run = () => {
if (defaultedOptions.value.enabled !== false) {
// fix #6133
observer.setOptions(defaultedOptions.value)
// Apply the latest options before resolving `enabled`, so a query callback sees the current query.
// fix #6133
observer.setOptions(defaultedOptions.value)
const { enabled } = defaultedOptions.value
if (
(typeof enabled === 'function'
? enabled(observer.getCurrentQuery())
: enabled) !== false
) {
const optimisticResult = observer.getOptimisticResult(
defaultedOptions.value,
)
Expand Down
14 changes: 6 additions & 8 deletions packages/vue-query/src/useInfiniteQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
DefinedInitialDataInfiniteOptions,
UndefinedInitialDataInfiniteOptions,
} from './infiniteQueryOptions'
import type { VueQueryEnabledOption } from './queryOptions'
import type {
DefaultError,
DefinedInfiniteQueryObserverResult,
Expand Down Expand Up @@ -47,14 +48,11 @@ export type UseInfiniteQueryOptions<
TPageParam
>
]: Property extends 'enabled'
? MaybeRefOrGetter<
InfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
DeepUnwrapRef<TQueryKey>,
TPageParam
>[Property]
? VueQueryEnabledOption<
TQueryFnData,
TError,
InfiniteData<TQueryFnData, TPageParam>,
DeepUnwrapRef<TQueryKey>
>
: MaybeRefDeep<
InfiniteQueryObserverOptions<
Expand Down
9 changes: 7 additions & 2 deletions packages/vue-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,13 @@ export function useQueries<
return queriesRaw.map((queryOptions) => {
const clonedOptions = cloneDeepUnref(queryOptions)

if (typeof clonedOptions.enabled === 'function') {
clonedOptions.enabled = queryOptions.enabled()
// A zero-arg function is a Vue getter and must run here so its refs are tracked.
// `(query) => boolean` is query-core's callback and is left for core to call.
if (
typeof clonedOptions.enabled === 'function' &&
clonedOptions.enabled.length === 0
) {
clonedOptions.enabled = clonedOptions.enabled()
}

const defaulted = client.defaultQueryOptions(clonedOptions)
Expand Down
Loading