Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to pass a callback to enabled. #7566

Merged
merged 12 commits into from
Jun 25, 2024
59 changes: 59 additions & 0 deletions packages/query-core/src/__tests__/queryObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,51 @@ describe('queryObserver', () => {
unsubscribe()
})

test('Should not refetch when enabled is a callback and returns false', async () => {
TkDodo marked this conversation as resolved.
Show resolved Hide resolved
const key = queryKey()
let count = 0
let enabled = true
const observer = new QueryObserver(queryClient, {
queryKey: key,
staleTime: Infinity,
enabled: () => enabled,
queryFn: async () => {
await sleep(10)
count++
return 'data'
},
})

let unsubscribe = observer.subscribe(vi.fn())

// unsubscribe before data comes in
unsubscribe()
expect(count).toBe(0)
expect(observer.getCurrentResult()).toMatchObject({
status: 'pending',
fetchStatus: 'fetching',
data: undefined,
})

await waitFor(() => expect(count).toBe(1))

// re-subscribe after data comes in
unsubscribe = observer.subscribe(vi.fn())

expect(observer.getCurrentResult()).toMatchObject({
status: 'success',
data: 'data',
})

enabled = false

queryClient.invalidateQueries({ queryKey: key, refetchType: 'all' })

expect(count).toBe(1)

unsubscribe()
})

test('should be able to read latest data when re-subscribing (but not re-fetching)', async () => {
const key = queryKey()
let count = 0
Expand Down Expand Up @@ -429,6 +474,20 @@ describe('queryObserver', () => {
expect(queryFn).toHaveBeenCalledTimes(0)
})

test('should not trigger a fetch when subscribed and disabled by callback', async () => {
const key = queryKey()
const queryFn = vi.fn<Array<unknown>, string>().mockReturnValue('data')
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn,
enabled: () => false,
})
const unsubscribe = observer.subscribe(() => undefined)
await sleep(1)
unsubscribe()
expect(queryFn).toHaveBeenCalledTimes(0)
})

test('should not trigger a fetch when not subscribed', async () => {
const key = queryKey()
const queryFn = vi.fn<Array<unknown>, string>().mockReturnValue('data')
Expand Down
12 changes: 10 additions & 2 deletions packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ensureQueryFn, noop, replaceData, timeUntilStale } from './utils'
import {
ensureQueryFn,
noop,
replaceData,
resolveEnabled,
timeUntilStale,
} from './utils'
import { notifyManager } from './notifyManager'
import { canFetch, createRetryer, isCancelledError } from './retryer'
import { Removable } from './removable'
Expand Down Expand Up @@ -244,7 +250,9 @@ export class Query<
}

isActive(): boolean {
return this.observers.some((observer) => observer.options.enabled !== false)
return this.observers.some(
(observer) => resolveEnabled(observer.options.enabled, this) !== false,
)
}

isDisabled(): boolean {
Expand Down
27 changes: 18 additions & 9 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
isValidTimeout,
noop,
replaceData,
resolveEnabled,
resolveStaleTime,
shallowEqualObjects,
timeUntilStale,
Expand Down Expand Up @@ -149,9 +150,14 @@ export class QueryObserver<

if (
this.options.enabled !== undefined &&
typeof this.options.enabled !== 'boolean'
typeof this.options.enabled !== 'boolean' &&
typeof this.options.enabled !== 'function' &&
typeof resolveEnabled(this.options.enabled, this.#currentQuery) !==
'boolean'
) {
throw new Error('Expected enabled to be a boolean')
throw new Error(
'Expected enabled to be a boolean or a callback that returns a boolean',
)
}

this.#updateQuery()
Expand Down Expand Up @@ -190,7 +196,8 @@ export class QueryObserver<
if (
mounted &&
(this.#currentQuery !== prevQuery ||
this.options.enabled !== prevOptions.enabled ||
resolveEnabled(this.options.enabled, this.#currentQuery) !==
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
resolveStaleTime(this.options.staleTime, this.#currentQuery) !==
resolveStaleTime(prevOptions.staleTime, this.#currentQuery))
) {
Expand All @@ -203,7 +210,8 @@ export class QueryObserver<
if (
mounted &&
(this.#currentQuery !== prevQuery ||
this.options.enabled !== prevOptions.enabled ||
resolveEnabled(this.options.enabled, this.#currentQuery) !==
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
nextRefetchInterval !== this.#currentRefetchInterval)
) {
this.#updateRefetchInterval(nextRefetchInterval)
Expand Down Expand Up @@ -377,7 +385,7 @@ export class QueryObserver<

if (
isServer ||
this.options.enabled === false ||
resolveEnabled(this.options.enabled, this.#currentQuery) === false ||
!isValidTimeout(this.#currentRefetchInterval) ||
this.#currentRefetchInterval === 0
) {
Expand Down Expand Up @@ -692,7 +700,7 @@ function shouldLoadOnMount(
options: QueryObserverOptions<any, any, any, any>,
): boolean {
return (
options.enabled !== false &&
resolveEnabled(options.enabled, query) !== false &&
query.state.data === undefined &&
!(query.state.status === 'error' && options.retryOnMount === false)
)
Expand All @@ -716,7 +724,7 @@ function shouldFetchOn(
(typeof options)['refetchOnWindowFocus'] &
(typeof options)['refetchOnReconnect'],
) {
if (options.enabled !== false) {
if (resolveEnabled(options.enabled, query) !== false) {
const value = typeof field === 'function' ? field(query) : field

return value === 'always' || (value !== false && isStale(query, options))
Expand All @@ -731,7 +739,8 @@ function shouldFetchOptionally(
prevOptions: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
(query !== prevQuery || prevOptions.enabled === false) &&
(query !== prevQuery ||
resolveEnabled(prevOptions.enabled, query) === false) &&
(!options.suspense || query.state.status !== 'error') &&
isStale(query, options)
)
Expand All @@ -742,7 +751,7 @@ function isStale(
options: QueryObserverOptions<any, any, any, any, any>,
): boolean {
return (
options.enabled !== false &&
resolveEnabled(options.enabled, query) !== false &&
query.isStaleByTime(resolveStaleTime(options.staleTime, query))
)
}
Expand Down
12 changes: 11 additions & 1 deletion packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export type StaleTime<
TQueryKey extends QueryKey = QueryKey,
> = number | ((query: Query<TQueryFnData, TError, TData, TQueryKey>) => number)

export type Enabled<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> =
| boolean
| ((query: Query<TQueryFnData, TError, TData, TQueryKey>) => boolean)

export type QueryPersister<
T = unknown,
TQueryKey extends QueryKey = QueryKey,
Expand Down Expand Up @@ -255,9 +264,10 @@ export interface QueryObserverOptions<
/**
* Set this to `false` to disable automatic refetching when the query mounts or changes query keys.
* To refetch the query, use the `refetch` method returned from the `useQuery` instance.
* You can also pass a callback that returns a boolean to check this condition dynamically.
* Defaults to `true`.
*/
enabled?: boolean
enabled?: Enabled<TQueryFnData, TError, TQueryData, TQueryKey>
/**
* The time in milliseconds after data is considered stale.
* If set to `Infinity`, the data will never be considered stale.
Expand Down
13 changes: 13 additions & 0 deletions packages/query-core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
DefaultError,
Enabled,
FetchStatus,
MutationKey,
MutationStatus,
Expand Down Expand Up @@ -100,6 +101,18 @@ export function resolveStaleTime<
return typeof staleTime === 'function' ? staleTime(query) : staleTime
}

export function resolveEnabled<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
enabled: undefined | Enabled<TQueryFnData, TError, TData, TQueryKey>,
query: Query<TQueryFnData, TError, TData, TQueryKey>,
): boolean | undefined {
return typeof enabled === 'function' ? enabled(query) : enabled
}

export function matchQuery(
filters: QueryFilters,
query: Query<any, any, any, any>,
Expand Down
Loading