Skip to content

Commit 31b9ab4

Browse files
lotusjohnTkDodoJohn Pettersson
authoredJun 25, 2024··
feat(core): Add possibility to pass a callback to enabled. (#7566)
* Add possibility to pass a callback to enabled. * Refactor into using the same pattern as resolving staletime, with the use of a resolveEnabled util function * Update tests for enabled option as a callback * update docs * remove typo * Update enabled type in docs * remove duplicated test case * Fix eslint errors --------- Co-authored-by: Dominik Dorfmeister <office@dorfmeister.cc> Co-authored-by: John Pettersson <john.pettersson@carnegie.se>
1 parent 461f3af commit 31b9ab4

File tree

8 files changed

+267
-15
lines changed

8 files changed

+267
-15
lines changed
 

‎docs/framework/react/guides/disabling-queries.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ id: disabling-queries
33
title: Disabling/Pausing Queries
44
---
55

6-
If you ever want to disable a query from automatically running, you can use the `enabled = false` option.
6+
If you ever want to disable a query from automatically running, you can use the `enabled = false` option. The enabled option also accepts a callback that returns a boolean.
77

88
When `enabled` is `false`:
99

‎docs/framework/react/react-native.md

+31
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,34 @@ function MyComponent() {
143143
return <Text>DataUpdatedAt: {dataUpdatedAt}</Text>
144144
}
145145
```
146+
147+
## Disable queries on out of focus screens
148+
149+
Enabled can also be set to a callback to support disabling queries on out of focus screens without state and re-rendering on navigation, similar to how notifyOnChangeProps works but in addition it wont trigger refetching when invalidating queries with refetchType active.
150+
151+
```tsx
152+
import React from 'react'
153+
import { useFocusEffect } from '@react-navigation/native'
154+
155+
export function useQueryFocusAware(notifyOnChangeProps?: NotifyOnChangeProps) {
156+
const focusedRef = React.useRef(true)
157+
158+
useFocusEffect(
159+
React.useCallback(() => {
160+
focusedRef.current = true
161+
162+
return () => {
163+
focusedRef.current = false
164+
}
165+
}, []),
166+
)
167+
168+
return () => focusRef.current
169+
170+
useQuery({
171+
queryKey: ['key'],
172+
queryFn: () => fetch(...),
173+
enabled: () => focusedRef.current,
174+
})
175+
}
176+
```

‎docs/framework/react/reference/useQuery.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const {
7070
- The function that the query will use to request data.
7171
- Receives a [QueryFunctionContext](../../guides/query-functions#queryfunctioncontext)
7272
- Must return a promise that will either resolve data or throw an error. The data cannot be `undefined`.
73-
- `enabled: boolean`
73+
- `enabled: boolean | (query: Query) => boolean`
7474
- Set this to `false` to disable this query from automatically running.
7575
- Can be used for [Dependent Queries](../../guides/dependent-queries).
7676
- `networkMode: 'online' | 'always' | 'offlineFirst`

‎packages/query-core/src/__tests__/queryObserver.test.tsx

+181
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,173 @@ describe('queryObserver', () => {
5252
unsubscribe()
5353
})
5454

55+
describe('enabled is a callback that initially returns false', () => {
56+
let observer: QueryObserver<string, Error, string, string, Array<string>>
57+
let enabled: boolean
58+
let count: number
59+
let key: Array<string>
60+
61+
beforeEach(() => {
62+
key = queryKey()
63+
count = 0
64+
enabled = false
65+
66+
observer = new QueryObserver(queryClient, {
67+
queryKey: key,
68+
staleTime: Infinity,
69+
enabled: () => enabled,
70+
queryFn: async () => {
71+
await sleep(10)
72+
count++
73+
return 'data'
74+
},
75+
})
76+
})
77+
78+
test('should not fetch on mount', () => {
79+
const unsubscribe = observer.subscribe(vi.fn())
80+
81+
// Has not fetched and is not fetching since its disabled
82+
expect(count).toBe(0)
83+
expect(observer.getCurrentResult()).toMatchObject({
84+
status: 'pending',
85+
fetchStatus: 'idle',
86+
data: undefined,
87+
})
88+
89+
unsubscribe()
90+
})
91+
92+
test('should not be re-fetched when invalidated with refetchType: all', async () => {
93+
const unsubscribe = observer.subscribe(vi.fn())
94+
95+
queryClient.invalidateQueries({ queryKey: key, refetchType: 'all' })
96+
97+
//So we still expect it to not have fetched and not be fetching
98+
expect(count).toBe(0)
99+
expect(observer.getCurrentResult()).toMatchObject({
100+
status: 'pending',
101+
fetchStatus: 'idle',
102+
data: undefined,
103+
})
104+
await waitFor(() => expect(count).toBe(0))
105+
106+
unsubscribe()
107+
})
108+
109+
test('should still trigger a fetch when refetch is called', async () => {
110+
const unsubscribe = observer.subscribe(vi.fn())
111+
112+
expect(enabled).toBe(false)
113+
114+
//Not the same with explicit refetch, this will override enabled and trigger a fetch anyway
115+
observer.refetch()
116+
117+
expect(observer.getCurrentResult()).toMatchObject({
118+
status: 'pending',
119+
fetchStatus: 'fetching',
120+
data: undefined,
121+
})
122+
123+
await waitFor(() => expect(count).toBe(1))
124+
expect(observer.getCurrentResult()).toMatchObject({
125+
status: 'success',
126+
fetchStatus: 'idle',
127+
data: 'data',
128+
})
129+
130+
unsubscribe()
131+
})
132+
133+
test('should fetch if unsubscribed, then enabled returns true, and then re-subscribed', async () => {
134+
let unsubscribe = observer.subscribe(vi.fn())
135+
expect(observer.getCurrentResult()).toMatchObject({
136+
status: 'pending',
137+
fetchStatus: 'idle',
138+
data: undefined,
139+
})
140+
141+
unsubscribe()
142+
143+
enabled = true
144+
145+
unsubscribe = observer.subscribe(vi.fn())
146+
147+
expect(observer.getCurrentResult()).toMatchObject({
148+
status: 'pending',
149+
fetchStatus: 'fetching',
150+
data: undefined,
151+
})
152+
153+
await waitFor(() => expect(count).toBe(1))
154+
155+
unsubscribe()
156+
})
157+
158+
test('should not be re-fetched if not subscribed to after enabled was toggled to true', async () => {
159+
const unsubscribe = observer.subscribe(vi.fn())
160+
161+
// Toggle enabled
162+
enabled = true
163+
164+
unsubscribe()
165+
166+
queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })
167+
168+
expect(observer.getCurrentResult()).toMatchObject({
169+
status: 'pending',
170+
fetchStatus: 'idle',
171+
data: undefined,
172+
})
173+
expect(count).toBe(0)
174+
})
175+
176+
test('should not be re-fetched if not subscribed to after enabled was toggled to true', async () => {
177+
const unsubscribe = observer.subscribe(vi.fn())
178+
179+
// Toggle enabled
180+
enabled = true
181+
182+
queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })
183+
184+
expect(observer.getCurrentResult()).toMatchObject({
185+
status: 'pending',
186+
fetchStatus: 'fetching',
187+
data: undefined,
188+
})
189+
await waitFor(() => expect(count).toBe(1))
190+
191+
unsubscribe()
192+
})
193+
194+
test('should handle that the enabled callback updates the return value', async () => {
195+
const unsubscribe = observer.subscribe(vi.fn())
196+
197+
// Toggle enabled
198+
enabled = true
199+
200+
queryClient.invalidateQueries({ queryKey: key, refetchType: 'inactive' })
201+
202+
//should not refetch since it was active and we only refetch inactive
203+
await waitFor(() => expect(count).toBe(0))
204+
205+
queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })
206+
207+
//should refetch since it was active and we refetch active
208+
await waitFor(() => expect(count).toBe(1))
209+
210+
// Toggle enabled
211+
enabled = false
212+
213+
//should not refetch since it is not active and we only refetch active
214+
queryClient.invalidateQueries({ queryKey: key, refetchType: 'active' })
215+
216+
await waitFor(() => expect(count).toBe(1))
217+
218+
unsubscribe()
219+
})
220+
})
221+
55222
test('should be able to read latest data when re-subscribing (but not re-fetching)', async () => {
56223
const key = queryKey()
57224
let count = 0
@@ -429,6 +596,20 @@ describe('queryObserver', () => {
429596
expect(queryFn).toHaveBeenCalledTimes(0)
430597
})
431598

599+
test('should not trigger a fetch when subscribed and disabled by callback', async () => {
600+
const key = queryKey()
601+
const queryFn = vi.fn<Array<unknown>, string>().mockReturnValue('data')
602+
const observer = new QueryObserver(queryClient, {
603+
queryKey: key,
604+
queryFn,
605+
enabled: () => false,
606+
})
607+
const unsubscribe = observer.subscribe(() => undefined)
608+
await sleep(1)
609+
unsubscribe()
610+
expect(queryFn).toHaveBeenCalledTimes(0)
611+
})
612+
432613
test('should not trigger a fetch when not subscribed', async () => {
433614
const key = queryKey()
434615
const queryFn = vi.fn<Array<unknown>, string>().mockReturnValue('data')

‎packages/query-core/src/query.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { ensureQueryFn, noop, replaceData, timeUntilStale } from './utils'
1+
import {
2+
ensureQueryFn,
3+
noop,
4+
replaceData,
5+
resolveEnabled,
6+
timeUntilStale,
7+
} from './utils'
28
import { notifyManager } from './notifyManager'
39
import { canFetch, createRetryer, isCancelledError } from './retryer'
410
import { Removable } from './removable'
@@ -244,7 +250,9 @@ export class Query<
244250
}
245251

246252
isActive(): boolean {
247-
return this.observers.some((observer) => observer.options.enabled !== false)
253+
return this.observers.some(
254+
(observer) => resolveEnabled(observer.options.enabled, this) !== false,
255+
)
248256
}
249257

250258
isDisabled(): boolean {

‎packages/query-core/src/queryObserver.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
isValidTimeout,
44
noop,
55
replaceData,
6+
resolveEnabled,
67
resolveStaleTime,
78
shallowEqualObjects,
89
timeUntilStale,
@@ -149,9 +150,14 @@ export class QueryObserver<
149150

150151
if (
151152
this.options.enabled !== undefined &&
152-
typeof this.options.enabled !== 'boolean'
153+
typeof this.options.enabled !== 'boolean' &&
154+
typeof this.options.enabled !== 'function' &&
155+
typeof resolveEnabled(this.options.enabled, this.#currentQuery) !==
156+
'boolean'
153157
) {
154-
throw new Error('Expected enabled to be a boolean')
158+
throw new Error(
159+
'Expected enabled to be a boolean or a callback that returns a boolean',
160+
)
155161
}
156162

157163
this.#updateQuery()
@@ -190,7 +196,8 @@ export class QueryObserver<
190196
if (
191197
mounted &&
192198
(this.#currentQuery !== prevQuery ||
193-
this.options.enabled !== prevOptions.enabled ||
199+
resolveEnabled(this.options.enabled, this.#currentQuery) !==
200+
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
194201
resolveStaleTime(this.options.staleTime, this.#currentQuery) !==
195202
resolveStaleTime(prevOptions.staleTime, this.#currentQuery))
196203
) {
@@ -203,7 +210,8 @@ export class QueryObserver<
203210
if (
204211
mounted &&
205212
(this.#currentQuery !== prevQuery ||
206-
this.options.enabled !== prevOptions.enabled ||
213+
resolveEnabled(this.options.enabled, this.#currentQuery) !==
214+
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
207215
nextRefetchInterval !== this.#currentRefetchInterval)
208216
) {
209217
this.#updateRefetchInterval(nextRefetchInterval)
@@ -377,7 +385,7 @@ export class QueryObserver<
377385

378386
if (
379387
isServer ||
380-
this.options.enabled === false ||
388+
resolveEnabled(this.options.enabled, this.#currentQuery) === false ||
381389
!isValidTimeout(this.#currentRefetchInterval) ||
382390
this.#currentRefetchInterval === 0
383391
) {
@@ -692,7 +700,7 @@ function shouldLoadOnMount(
692700
options: QueryObserverOptions<any, any, any, any>,
693701
): boolean {
694702
return (
695-
options.enabled !== false &&
703+
resolveEnabled(options.enabled, query) !== false &&
696704
query.state.data === undefined &&
697705
!(query.state.status === 'error' && options.retryOnMount === false)
698706
)
@@ -716,7 +724,7 @@ function shouldFetchOn(
716724
(typeof options)['refetchOnWindowFocus'] &
717725
(typeof options)['refetchOnReconnect'],
718726
) {
719-
if (options.enabled !== false) {
727+
if (resolveEnabled(options.enabled, query) !== false) {
720728
const value = typeof field === 'function' ? field(query) : field
721729

722730
return value === 'always' || (value !== false && isStale(query, options))
@@ -731,7 +739,8 @@ function shouldFetchOptionally(
731739
prevOptions: QueryObserverOptions<any, any, any, any, any>,
732740
): boolean {
733741
return (
734-
(query !== prevQuery || prevOptions.enabled === false) &&
742+
(query !== prevQuery ||
743+
resolveEnabled(prevOptions.enabled, query) === false) &&
735744
(!options.suspense || query.state.status !== 'error') &&
736745
isStale(query, options)
737746
)
@@ -742,7 +751,7 @@ function isStale(
742751
options: QueryObserverOptions<any, any, any, any, any>,
743752
): boolean {
744753
return (
745-
options.enabled !== false &&
754+
resolveEnabled(options.enabled, query) !== false &&
746755
query.isStaleByTime(resolveStaleTime(options.staleTime, query))
747756
)
748757
}

‎packages/query-core/src/types.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ export type StaleTime<
5454
TQueryKey extends QueryKey = QueryKey,
5555
> = number | ((query: Query<TQueryFnData, TError, TData, TQueryKey>) => number)
5656

57+
export type Enabled<
58+
TQueryFnData = unknown,
59+
TError = DefaultError,
60+
TData = TQueryFnData,
61+
TQueryKey extends QueryKey = QueryKey,
62+
> =
63+
| boolean
64+
| ((query: Query<TQueryFnData, TError, TData, TQueryKey>) => boolean)
65+
5766
export type QueryPersister<
5867
T = unknown,
5968
TQueryKey extends QueryKey = QueryKey,
@@ -253,11 +262,12 @@ export interface QueryObserverOptions<
253262
'queryKey'
254263
> {
255264
/**
256-
* Set this to `false` to disable automatic refetching when the query mounts or changes query keys.
265+
* Set this to `false` or a function that returns `false` to disable automatic refetching when the query mounts or changes query keys.
257266
* To refetch the query, use the `refetch` method returned from the `useQuery` instance.
267+
* Accepts a boolean or function that returns a boolean.
258268
* Defaults to `true`.
259269
*/
260-
enabled?: boolean
270+
enabled?: Enabled<TQueryFnData, TError, TQueryData, TQueryKey>
261271
/**
262272
* The time in milliseconds after data is considered stale.
263273
* If set to `Infinity`, the data will never be considered stale.

‎packages/query-core/src/utils.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
DefaultError,
3+
Enabled,
34
FetchStatus,
45
MutationKey,
56
MutationStatus,
@@ -100,6 +101,18 @@ export function resolveStaleTime<
100101
return typeof staleTime === 'function' ? staleTime(query) : staleTime
101102
}
102103

104+
export function resolveEnabled<
105+
TQueryFnData = unknown,
106+
TError = DefaultError,
107+
TData = TQueryFnData,
108+
TQueryKey extends QueryKey = QueryKey,
109+
>(
110+
enabled: undefined | Enabled<TQueryFnData, TError, TData, TQueryKey>,
111+
query: Query<TQueryFnData, TError, TData, TQueryKey>,
112+
): boolean | undefined {
113+
return typeof enabled === 'function' ? enabled(query) : enabled
114+
}
115+
103116
export function matchQuery(
104117
filters: QueryFilters,
105118
query: Query<any, any, any, any>,

0 commit comments

Comments
 (0)
Please sign in to comment.