@tanstack/query-core 5.47.0 → 5.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/build/legacy/focusManager.cjs.map +1 -1
  2. package/build/legacy/focusManager.js.map +1 -1
  3. package/build/legacy/{hydration-BZ2M_xzi.d.ts → hydration-ByKLEQMr.d.ts} +9 -4
  4. package/build/legacy/{hydration-CwKUqoFl.d.cts → hydration-DfqGmvZi.d.cts} +9 -4
  5. package/build/legacy/hydration.cjs +32 -15
  6. package/build/legacy/hydration.cjs.map +1 -1
  7. package/build/legacy/hydration.d.cts +1 -1
  8. package/build/legacy/hydration.d.ts +1 -1
  9. package/build/legacy/hydration.js +32 -15
  10. package/build/legacy/hydration.js.map +1 -1
  11. package/build/legacy/index.d.cts +1 -1
  12. package/build/legacy/index.d.ts +1 -1
  13. package/build/legacy/infiniteQueryBehavior.d.cts +1 -1
  14. package/build/legacy/infiniteQueryBehavior.d.ts +1 -1
  15. package/build/legacy/infiniteQueryObserver.d.cts +1 -1
  16. package/build/legacy/infiniteQueryObserver.d.ts +1 -1
  17. package/build/legacy/mutation.d.cts +1 -1
  18. package/build/legacy/mutation.d.ts +1 -1
  19. package/build/legacy/mutationCache.d.cts +1 -1
  20. package/build/legacy/mutationCache.d.ts +1 -1
  21. package/build/legacy/mutationObserver.d.cts +1 -1
  22. package/build/legacy/mutationObserver.d.ts +1 -1
  23. package/build/legacy/onlineManager.cjs.map +1 -1
  24. package/build/legacy/onlineManager.js.map +1 -1
  25. package/build/legacy/queriesObserver.d.cts +1 -1
  26. package/build/legacy/queriesObserver.d.ts +1 -1
  27. package/build/legacy/query.cjs +3 -1
  28. package/build/legacy/query.cjs.map +1 -1
  29. package/build/legacy/query.d.cts +1 -1
  30. package/build/legacy/query.d.ts +1 -1
  31. package/build/legacy/query.js +10 -2
  32. package/build/legacy/query.js.map +1 -1
  33. package/build/legacy/queryCache.d.cts +1 -1
  34. package/build/legacy/queryCache.d.ts +1 -1
  35. package/build/legacy/queryClient.cjs.map +1 -1
  36. package/build/legacy/queryClient.d.cts +1 -1
  37. package/build/legacy/queryClient.d.ts +1 -1
  38. package/build/legacy/queryClient.js.map +1 -1
  39. package/build/legacy/queryObserver.cjs +11 -9
  40. package/build/legacy/queryObserver.cjs.map +1 -1
  41. package/build/legacy/queryObserver.d.cts +1 -1
  42. package/build/legacy/queryObserver.d.ts +1 -1
  43. package/build/legacy/queryObserver.js +12 -9
  44. package/build/legacy/queryObserver.js.map +1 -1
  45. package/build/legacy/retryer.d.cts +1 -1
  46. package/build/legacy/retryer.d.ts +1 -1
  47. package/build/legacy/types.cjs.map +1 -1
  48. package/build/legacy/types.d.cts +1 -1
  49. package/build/legacy/types.d.ts +1 -1
  50. package/build/legacy/utils.cjs +5 -0
  51. package/build/legacy/utils.cjs.map +1 -1
  52. package/build/legacy/utils.d.cts +1 -1
  53. package/build/legacy/utils.d.ts +1 -1
  54. package/build/legacy/utils.js +4 -0
  55. package/build/legacy/utils.js.map +1 -1
  56. package/build/modern/focusManager.cjs.map +1 -1
  57. package/build/modern/focusManager.js.map +1 -1
  58. package/build/modern/{hydration-BZ2M_xzi.d.ts → hydration-ByKLEQMr.d.ts} +9 -4
  59. package/build/modern/{hydration-CwKUqoFl.d.cts → hydration-DfqGmvZi.d.cts} +9 -4
  60. package/build/modern/hydration.cjs +24 -8
  61. package/build/modern/hydration.cjs.map +1 -1
  62. package/build/modern/hydration.d.cts +1 -1
  63. package/build/modern/hydration.d.ts +1 -1
  64. package/build/modern/hydration.js +24 -8
  65. package/build/modern/hydration.js.map +1 -1
  66. package/build/modern/index.d.cts +1 -1
  67. package/build/modern/index.d.ts +1 -1
  68. package/build/modern/infiniteQueryBehavior.d.cts +1 -1
  69. package/build/modern/infiniteQueryBehavior.d.ts +1 -1
  70. package/build/modern/infiniteQueryObserver.d.cts +1 -1
  71. package/build/modern/infiniteQueryObserver.d.ts +1 -1
  72. package/build/modern/mutation.d.cts +1 -1
  73. package/build/modern/mutation.d.ts +1 -1
  74. package/build/modern/mutationCache.d.cts +1 -1
  75. package/build/modern/mutationCache.d.ts +1 -1
  76. package/build/modern/mutationObserver.d.cts +1 -1
  77. package/build/modern/mutationObserver.d.ts +1 -1
  78. package/build/modern/onlineManager.cjs.map +1 -1
  79. package/build/modern/onlineManager.js.map +1 -1
  80. package/build/modern/queriesObserver.d.cts +1 -1
  81. package/build/modern/queriesObserver.d.ts +1 -1
  82. package/build/modern/query.cjs +3 -1
  83. package/build/modern/query.cjs.map +1 -1
  84. package/build/modern/query.d.cts +1 -1
  85. package/build/modern/query.d.ts +1 -1
  86. package/build/modern/query.js +10 -2
  87. package/build/modern/query.js.map +1 -1
  88. package/build/modern/queryCache.d.cts +1 -1
  89. package/build/modern/queryCache.d.ts +1 -1
  90. package/build/modern/queryClient.cjs.map +1 -1
  91. package/build/modern/queryClient.d.cts +1 -1
  92. package/build/modern/queryClient.d.ts +1 -1
  93. package/build/modern/queryClient.js.map +1 -1
  94. package/build/modern/queryObserver.cjs +11 -9
  95. package/build/modern/queryObserver.cjs.map +1 -1
  96. package/build/modern/queryObserver.d.cts +1 -1
  97. package/build/modern/queryObserver.d.ts +1 -1
  98. package/build/modern/queryObserver.js +12 -9
  99. package/build/modern/queryObserver.js.map +1 -1
  100. package/build/modern/retryer.d.cts +1 -1
  101. package/build/modern/retryer.d.ts +1 -1
  102. package/build/modern/types.cjs.map +1 -1
  103. package/build/modern/types.d.cts +1 -1
  104. package/build/modern/types.d.ts +1 -1
  105. package/build/modern/utils.cjs +5 -0
  106. package/build/modern/utils.cjs.map +1 -1
  107. package/build/modern/utils.d.cts +1 -1
  108. package/build/modern/utils.d.ts +1 -1
  109. package/build/modern/utils.js +4 -0
  110. package/build/modern/utils.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/__tests__/hydration.test.tsx +85 -2
  113. package/src/__tests__/queryObserver.test.tsx +181 -0
  114. package/src/focusManager.ts +2 -2
  115. package/src/hydration.ts +41 -14
  116. package/src/onlineManager.ts +1 -1
  117. package/src/query.ts +10 -2
  118. package/src/queryClient.ts +3 -1
  119. package/src/queryObserver.ts +18 -9
  120. package/src/types.ts +12 -2
  121. package/src/utils.ts +13 -0
@@ -52,6 +52,173 @@ describe('queryObserver', () => {
52
52
  unsubscribe()
53
53
  })
54
54
 
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
+
55
222
  test('should be able to read latest data when re-subscribing (but not re-fetching)', async () => {
56
223
  const key = queryKey()
57
224
  let count = 0
@@ -429,6 +596,20 @@ describe('queryObserver', () => {
429
596
  expect(queryFn).toHaveBeenCalledTimes(0)
430
597
  })
431
598
 
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
+
432
613
  test('should not trigger a fetch when not subscribed', async () => {
433
614
  const key = queryKey()
434
615
  const queryFn = vi.fn<Array<unknown>, string>().mockReturnValue('data')
@@ -17,7 +17,7 @@ export class FocusManager extends Subscribable<Listener> {
17
17
  super()
18
18
  this.#setup = (onFocus) => {
19
19
  // addEventListener does not exist in React Native, but window does
20
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
20
+ // eslint-disable-next-line ts/no-unnecessary-condition
21
21
  if (!isServer && window.addEventListener) {
22
22
  const listener = () => onFocus()
23
23
  // Listen to visibilitychange
@@ -78,7 +78,7 @@ export class FocusManager extends Subscribable<Listener> {
78
78
  }
79
79
 
80
80
  // document global can be unavailable in react native
81
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
81
+ // eslint-disable-next-line ts/no-unnecessary-condition
82
82
  return globalThis.document?.visibilityState !== 'hidden'
83
83
  }
84
84
  }
package/src/hydration.ts CHANGED
@@ -13,15 +13,20 @@ import type { Query, QueryState } from './query'
13
13
  import type { Mutation, MutationState } from './mutation'
14
14
 
15
15
  // TYPES
16
+ type TransformerFn = (data: any) => any
17
+ function defaultTransformerFn(data: any): any {
18
+ return data
19
+ }
16
20
 
17
21
  export interface DehydrateOptions {
22
+ serializeData?: TransformerFn
18
23
  shouldDehydrateMutation?: (mutation: Mutation) => boolean
19
24
  shouldDehydrateQuery?: (query: Query) => boolean
20
25
  }
21
26
 
22
27
  export interface HydrateOptions {
23
28
  defaultOptions?: {
24
- transformPromise?: (promise: Promise<any>) => Promise<any>
29
+ deserializeData?: TransformerFn
25
30
  queries?: QueryOptions
26
31
  mutations?: MutationOptions<unknown, DefaultError, unknown, unknown>
27
32
  }
@@ -62,13 +67,21 @@ function dehydrateMutation(mutation: Mutation): DehydratedMutation {
62
67
  // consuming the de/rehydrated data, typically with useQuery on the client.
63
68
  // Sometimes it might make sense to prefetch data on the server and include
64
69
  // in the html-payload, but not consume it on the initial render.
65
- function dehydrateQuery(query: Query): DehydratedQuery {
70
+ function dehydrateQuery(
71
+ query: Query,
72
+ serializeData: TransformerFn,
73
+ ): DehydratedQuery {
66
74
  return {
67
- state: query.state,
75
+ state: {
76
+ ...query.state,
77
+ ...(query.state.data !== undefined && {
78
+ data: serializeData(query.state.data),
79
+ }),
80
+ },
68
81
  queryKey: query.queryKey,
69
82
  queryHash: query.queryHash,
70
83
  ...(query.state.status === 'pending' && {
71
- promise: query.promise?.catch((error) => {
84
+ promise: query.promise?.then(serializeData).catch((error) => {
72
85
  if (process.env.NODE_ENV !== 'production') {
73
86
  console.error(
74
87
  `A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`,
@@ -110,10 +123,17 @@ export function dehydrate(
110
123
  client.getDefaultOptions().dehydrate?.shouldDehydrateQuery ??
111
124
  defaultShouldDehydrateQuery
112
125
 
126
+ const serializeData =
127
+ options.serializeData ??
128
+ client.getDefaultOptions().dehydrate?.serializeData ??
129
+ defaultTransformerFn
130
+
113
131
  const queries = client
114
132
  .getQueryCache()
115
133
  .getAll()
116
- .flatMap((query) => (filterQuery(query) ? [dehydrateQuery(query)] : []))
134
+ .flatMap((query) =>
135
+ filterQuery(query) ? [dehydrateQuery(query, serializeData)] : [],
136
+ )
117
137
 
118
138
  return { mutations, queries }
119
139
  }
@@ -129,10 +149,14 @@ export function hydrate(
129
149
 
130
150
  const mutationCache = client.getMutationCache()
131
151
  const queryCache = client.getQueryCache()
152
+ const deserializeData =
153
+ options?.defaultOptions?.deserializeData ??
154
+ client.getDefaultOptions().hydrate?.deserializeData ??
155
+ defaultTransformerFn
132
156
 
133
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
157
+ // eslint-disable-next-line ts/no-unnecessary-condition
134
158
  const mutations = (dehydratedState as DehydratedState).mutations || []
135
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
159
+ // eslint-disable-next-line ts/no-unnecessary-condition
136
160
  const queries = (dehydratedState as DehydratedState).queries || []
137
161
 
138
162
  mutations.forEach(({ state, ...mutationOptions }) => {
@@ -150,13 +174,19 @@ export function hydrate(
150
174
  queries.forEach(({ queryKey, state, queryHash, meta, promise }) => {
151
175
  let query = queryCache.get(queryHash)
152
176
 
177
+ const data =
178
+ state.data === undefined ? state.data : deserializeData(state.data)
179
+
153
180
  // Do not hydrate if an existing query exists with newer data
154
181
  if (query) {
155
182
  if (query.state.dataUpdatedAt < state.dataUpdatedAt) {
156
183
  // omit fetchStatus from dehydrated state
157
184
  // so that query stays in its current fetchStatus
158
- const { fetchStatus: _ignored, ...dehydratedQueryState } = state
159
- query.setState(dehydratedQueryState)
185
+ const { fetchStatus: _ignored, ...serializedState } = state
186
+ query.setState({
187
+ ...serializedState,
188
+ data,
189
+ })
160
190
  }
161
191
  } else {
162
192
  // Restore query
@@ -173,19 +203,16 @@ export function hydrate(
173
203
  // query being stuck in fetching state upon hydration
174
204
  {
175
205
  ...state,
206
+ data,
176
207
  fetchStatus: 'idle',
177
208
  },
178
209
  )
179
210
  }
180
211
 
181
212
  if (promise) {
182
- const transformPromise =
183
- client.getDefaultOptions().hydrate?.transformPromise
184
-
185
213
  // Note: `Promise.resolve` required cause
186
214
  // RSC transformed promises are not thenable
187
- const initialPromise =
188
- transformPromise?.(Promise.resolve(promise)) ?? promise
215
+ const initialPromise = Promise.resolve(promise).then(deserializeData)
189
216
 
190
217
  // this doesn't actually fetch - it just creates a retryer
191
218
  // which will re-use the passed `initialPromise`
@@ -14,7 +14,7 @@ export class OnlineManager extends Subscribable<Listener> {
14
14
  super()
15
15
  this.#setup = (onOnline) => {
16
16
  // addEventListener does not exist in React Native, but window does
17
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
17
+ // eslint-disable-next-line ts/no-unnecessary-condition
18
18
  if (!isServer && window.addEventListener) {
19
19
  const onlineListener = () => onOnline(true)
20
20
  const offlineListener = () => onOnline(false)
package/src/query.ts CHANGED
@@ -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'
2
8
  import { notifyManager } from './notifyManager'
3
9
  import { canFetch, createRetryer, isCancelledError } from './retryer'
4
10
  import { Removable } from './removable'
@@ -244,7 +250,9 @@ export class Query<
244
250
  }
245
251
 
246
252
  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
+ )
248
256
  }
249
257
 
250
258
  isDisabled(): boolean {
@@ -13,10 +13,10 @@ import { focusManager } from './focusManager'
13
13
  import { onlineManager } from './onlineManager'
14
14
  import { notifyManager } from './notifyManager'
15
15
  import { infiniteQueryBehavior } from './infiniteQueryBehavior'
16
- import type { DataTag, NoInfer, OmitKeyof } from './types'
17
16
  import type { QueryState } from './query'
18
17
  import type {
19
18
  CancelOptions,
19
+ DataTag,
20
20
  DefaultError,
21
21
  DefaultOptions,
22
22
  DefaultedQueryObserverOptions,
@@ -29,6 +29,8 @@ import type {
29
29
  MutationKey,
30
30
  MutationObserverOptions,
31
31
  MutationOptions,
32
+ NoInfer,
33
+ OmitKeyof,
32
34
  QueryClientConfig,
33
35
  QueryKey,
34
36
  QueryObserverOptions,
@@ -3,6 +3,7 @@ import {
3
3
  isValidTimeout,
4
4
  noop,
5
5
  replaceData,
6
+ resolveEnabled,
6
7
  resolveStaleTime,
7
8
  shallowEqualObjects,
8
9
  timeUntilStale,
@@ -149,9 +150,14 @@ export class QueryObserver<
149
150
 
150
151
  if (
151
152
  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'
153
157
  ) {
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
+ )
155
161
  }
156
162
 
157
163
  this.#updateQuery()
@@ -190,7 +196,8 @@ export class QueryObserver<
190
196
  if (
191
197
  mounted &&
192
198
  (this.#currentQuery !== prevQuery ||
193
- this.options.enabled !== prevOptions.enabled ||
199
+ resolveEnabled(this.options.enabled, this.#currentQuery) !==
200
+ resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
194
201
  resolveStaleTime(this.options.staleTime, this.#currentQuery) !==
195
202
  resolveStaleTime(prevOptions.staleTime, this.#currentQuery))
196
203
  ) {
@@ -203,7 +210,8 @@ export class QueryObserver<
203
210
  if (
204
211
  mounted &&
205
212
  (this.#currentQuery !== prevQuery ||
206
- this.options.enabled !== prevOptions.enabled ||
213
+ resolveEnabled(this.options.enabled, this.#currentQuery) !==
214
+ resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
207
215
  nextRefetchInterval !== this.#currentRefetchInterval)
208
216
  ) {
209
217
  this.#updateRefetchInterval(nextRefetchInterval)
@@ -377,7 +385,7 @@ export class QueryObserver<
377
385
 
378
386
  if (
379
387
  isServer ||
380
- this.options.enabled === false ||
388
+ resolveEnabled(this.options.enabled, this.#currentQuery) === false ||
381
389
  !isValidTimeout(this.#currentRefetchInterval) ||
382
390
  this.#currentRefetchInterval === 0
383
391
  ) {
@@ -692,7 +700,7 @@ function shouldLoadOnMount(
692
700
  options: QueryObserverOptions<any, any, any, any>,
693
701
  ): boolean {
694
702
  return (
695
- options.enabled !== false &&
703
+ resolveEnabled(options.enabled, query) !== false &&
696
704
  query.state.data === undefined &&
697
705
  !(query.state.status === 'error' && options.retryOnMount === false)
698
706
  )
@@ -716,7 +724,7 @@ function shouldFetchOn(
716
724
  (typeof options)['refetchOnWindowFocus'] &
717
725
  (typeof options)['refetchOnReconnect'],
718
726
  ) {
719
- if (options.enabled !== false) {
727
+ if (resolveEnabled(options.enabled, query) !== false) {
720
728
  const value = typeof field === 'function' ? field(query) : field
721
729
 
722
730
  return value === 'always' || (value !== false && isStale(query, options))
@@ -731,7 +739,8 @@ function shouldFetchOptionally(
731
739
  prevOptions: QueryObserverOptions<any, any, any, any, any>,
732
740
  ): boolean {
733
741
  return (
734
- (query !== prevQuery || prevOptions.enabled === false) &&
742
+ (query !== prevQuery ||
743
+ resolveEnabled(prevOptions.enabled, query) === false) &&
735
744
  (!options.suspense || query.state.status !== 'error') &&
736
745
  isStale(query, options)
737
746
  )
@@ -742,7 +751,7 @@ function isStale(
742
751
  options: QueryObserverOptions<any, any, any, any, any>,
743
752
  ): boolean {
744
753
  return (
745
- options.enabled !== false &&
754
+ resolveEnabled(options.enabled, query) !== false &&
746
755
  query.isStaleByTime(resolveStaleTime(options.staleTime, query))
747
756
  )
748
757
  }
package/src/types.ts CHANGED
@@ -54,6 +54,15 @@ export type StaleTime<
54
54
  TQueryKey extends QueryKey = QueryKey,
55
55
  > = number | ((query: Query<TQueryFnData, TError, TData, TQueryKey>) => number)
56
56
 
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
+
57
66
  export type QueryPersister<
58
67
  T = unknown,
59
68
  TQueryKey extends QueryKey = QueryKey,
@@ -253,11 +262,12 @@ export interface QueryObserverOptions<
253
262
  'queryKey'
254
263
  > {
255
264
  /**
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.
257
266
  * To refetch the query, use the `refetch` method returned from the `useQuery` instance.
267
+ * Accepts a boolean or function that returns a boolean.
258
268
  * Defaults to `true`.
259
269
  */
260
- enabled?: boolean
270
+ enabled?: Enabled<TQueryFnData, TError, TQueryData, TQueryKey>
261
271
  /**
262
272
  * The time in milliseconds after data is considered stale.
263
273
  * If set to `Infinity`, the data will never be considered stale.
package/src/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  DefaultError,
3
+ Enabled,
3
4
  FetchStatus,
4
5
  MutationKey,
5
6
  MutationStatus,
@@ -100,6 +101,18 @@ export function resolveStaleTime<
100
101
  return typeof staleTime === 'function' ? staleTime(query) : staleTime
101
102
  }
102
103
 
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
+
103
116
  export function matchQuery(
104
117
  filters: QueryFilters,
105
118
  query: Query<any, any, any, any>,