@tanstack/query-core 5.38.0 → 5.44.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 (138) hide show
  1. package/build/legacy/{types-BtrVwz9w.d.ts → hydration-DJZYTIMr.d.ts} +50 -6
  2. package/build/legacy/{types-BvcshvE9.d.cts → hydration-XP7CH-6g.d.cts} +50 -6
  3. package/build/legacy/hydration.cjs +40 -23
  4. package/build/legacy/hydration.cjs.map +1 -1
  5. package/build/legacy/hydration.d.cts +1 -34
  6. package/build/legacy/hydration.d.ts +1 -34
  7. package/build/legacy/hydration.js +40 -23
  8. package/build/legacy/hydration.js.map +1 -1
  9. package/build/legacy/index.d.cts +1 -2
  10. package/build/legacy/index.d.ts +1 -2
  11. package/build/legacy/infiniteQueryBehavior.cjs +1 -12
  12. package/build/legacy/infiniteQueryBehavior.cjs.map +1 -1
  13. package/build/legacy/infiniteQueryBehavior.d.cts +1 -1
  14. package/build/legacy/infiniteQueryBehavior.d.ts +1 -1
  15. package/build/legacy/infiniteQueryBehavior.js +2 -13
  16. package/build/legacy/infiniteQueryBehavior.js.map +1 -1
  17. package/build/legacy/infiniteQueryObserver.d.cts +1 -1
  18. package/build/legacy/infiniteQueryObserver.d.ts +1 -1
  19. package/build/legacy/mutation.d.cts +1 -1
  20. package/build/legacy/mutation.d.ts +1 -1
  21. package/build/legacy/mutationCache.d.cts +1 -1
  22. package/build/legacy/mutationCache.d.ts +1 -1
  23. package/build/legacy/mutationObserver.d.cts +1 -1
  24. package/build/legacy/mutationObserver.d.ts +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 +13 -21
  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 +14 -22
  32. package/build/legacy/query.js.map +1 -1
  33. package/build/legacy/queryCache.cjs.map +1 -1
  34. package/build/legacy/queryCache.d.cts +1 -1
  35. package/build/legacy/queryCache.d.ts +1 -1
  36. package/build/legacy/queryCache.js.map +1 -1
  37. package/build/legacy/queryClient.cjs +4 -2
  38. package/build/legacy/queryClient.cjs.map +1 -1
  39. package/build/legacy/queryClient.d.cts +1 -1
  40. package/build/legacy/queryClient.d.ts +1 -1
  41. package/build/legacy/queryClient.js +5 -2
  42. package/build/legacy/queryClient.js.map +1 -1
  43. package/build/legacy/queryObserver.cjs +8 -7
  44. package/build/legacy/queryObserver.cjs.map +1 -1
  45. package/build/legacy/queryObserver.d.cts +1 -1
  46. package/build/legacy/queryObserver.d.ts +1 -1
  47. package/build/legacy/queryObserver.js +9 -7
  48. package/build/legacy/queryObserver.js.map +1 -1
  49. package/build/legacy/retryer.cjs +2 -1
  50. package/build/legacy/retryer.cjs.map +1 -1
  51. package/build/legacy/retryer.d.cts +1 -1
  52. package/build/legacy/retryer.d.ts +1 -1
  53. package/build/legacy/retryer.js +2 -1
  54. package/build/legacy/retryer.js.map +1 -1
  55. package/build/legacy/types.cjs.map +1 -1
  56. package/build/legacy/types.d.cts +1 -1
  57. package/build/legacy/types.d.ts +1 -1
  58. package/build/legacy/utils.cjs +23 -0
  59. package/build/legacy/utils.cjs.map +1 -1
  60. package/build/legacy/utils.d.cts +1 -1
  61. package/build/legacy/utils.d.ts +1 -1
  62. package/build/legacy/utils.js +21 -0
  63. package/build/legacy/utils.js.map +1 -1
  64. package/build/modern/{types-BtrVwz9w.d.ts → hydration-DJZYTIMr.d.ts} +50 -6
  65. package/build/modern/{types-BvcshvE9.d.cts → hydration-XP7CH-6g.d.cts} +50 -6
  66. package/build/modern/hydration.cjs +35 -20
  67. package/build/modern/hydration.cjs.map +1 -1
  68. package/build/modern/hydration.d.cts +1 -34
  69. package/build/modern/hydration.d.ts +1 -34
  70. package/build/modern/hydration.js +35 -20
  71. package/build/modern/hydration.js.map +1 -1
  72. package/build/modern/index.d.cts +1 -2
  73. package/build/modern/index.d.ts +1 -2
  74. package/build/modern/infiniteQueryBehavior.cjs +1 -12
  75. package/build/modern/infiniteQueryBehavior.cjs.map +1 -1
  76. package/build/modern/infiniteQueryBehavior.d.cts +1 -1
  77. package/build/modern/infiniteQueryBehavior.d.ts +1 -1
  78. package/build/modern/infiniteQueryBehavior.js +2 -13
  79. package/build/modern/infiniteQueryBehavior.js.map +1 -1
  80. package/build/modern/infiniteQueryObserver.d.cts +1 -1
  81. package/build/modern/infiniteQueryObserver.d.ts +1 -1
  82. package/build/modern/mutation.d.cts +1 -1
  83. package/build/modern/mutation.d.ts +1 -1
  84. package/build/modern/mutationCache.d.cts +1 -1
  85. package/build/modern/mutationCache.d.ts +1 -1
  86. package/build/modern/mutationObserver.d.cts +1 -1
  87. package/build/modern/mutationObserver.d.ts +1 -1
  88. package/build/modern/queriesObserver.d.cts +1 -1
  89. package/build/modern/queriesObserver.d.ts +1 -1
  90. package/build/modern/query.cjs +12 -21
  91. package/build/modern/query.cjs.map +1 -1
  92. package/build/modern/query.d.cts +1 -1
  93. package/build/modern/query.d.ts +1 -1
  94. package/build/modern/query.js +13 -22
  95. package/build/modern/query.js.map +1 -1
  96. package/build/modern/queryCache.cjs.map +1 -1
  97. package/build/modern/queryCache.d.cts +1 -1
  98. package/build/modern/queryCache.d.ts +1 -1
  99. package/build/modern/queryCache.js.map +1 -1
  100. package/build/modern/queryClient.cjs +4 -2
  101. package/build/modern/queryClient.cjs.map +1 -1
  102. package/build/modern/queryClient.d.cts +1 -1
  103. package/build/modern/queryClient.d.ts +1 -1
  104. package/build/modern/queryClient.js +5 -2
  105. package/build/modern/queryClient.js.map +1 -1
  106. package/build/modern/queryObserver.cjs +8 -7
  107. package/build/modern/queryObserver.cjs.map +1 -1
  108. package/build/modern/queryObserver.d.cts +1 -1
  109. package/build/modern/queryObserver.d.ts +1 -1
  110. package/build/modern/queryObserver.js +9 -7
  111. package/build/modern/queryObserver.js.map +1 -1
  112. package/build/modern/retryer.cjs +2 -1
  113. package/build/modern/retryer.cjs.map +1 -1
  114. package/build/modern/retryer.d.cts +1 -1
  115. package/build/modern/retryer.d.ts +1 -1
  116. package/build/modern/retryer.js +2 -1
  117. package/build/modern/retryer.js.map +1 -1
  118. package/build/modern/types.cjs.map +1 -1
  119. package/build/modern/types.d.cts +1 -1
  120. package/build/modern/types.d.ts +1 -1
  121. package/build/modern/utils.cjs +23 -0
  122. package/build/modern/utils.cjs.map +1 -1
  123. package/build/modern/utils.d.cts +1 -1
  124. package/build/modern/utils.d.ts +1 -1
  125. package/build/modern/utils.js +21 -0
  126. package/build/modern/utils.js.map +1 -1
  127. package/package.json +1 -1
  128. package/src/__tests__/hydration.test.tsx +170 -0
  129. package/src/__tests__/queryObserver.test.tsx +26 -0
  130. package/src/hydration.ts +43 -21
  131. package/src/infiniteQueryBehavior.ts +2 -17
  132. package/src/query.ts +24 -29
  133. package/src/queryCache.ts +6 -1
  134. package/src/queryClient.ts +5 -2
  135. package/src/queryObserver.ts +14 -12
  136. package/src/retryer.ts +6 -1
  137. package/src/types.ts +13 -2
  138. package/src/utils.ts +50 -2
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test, vi } from 'vitest'
2
+ import { waitFor } from '@testing-library/react'
2
3
  import { QueryCache } from '../queryCache'
3
4
  import { dehydrate, hydrate } from '../hydration'
4
5
  import { MutationCache } from '../mutationCache'
@@ -174,6 +175,84 @@ describe('dehydration and rehydration', () => {
174
175
  hydrationClient.clear()
175
176
  })
176
177
 
178
+ test('should respect query defaultOptions specified on the QueryClient', async () => {
179
+ const queryCache = new QueryCache()
180
+ const queryClient = createQueryClient({
181
+ queryCache,
182
+ defaultOptions: {
183
+ dehydrate: { shouldDehydrateQuery: () => true },
184
+ },
185
+ })
186
+ await queryClient.prefetchQuery({
187
+ queryKey: ['string'],
188
+ retry: 0,
189
+ queryFn: () => Promise.reject(new Error('error')),
190
+ })
191
+ const dehydrated = dehydrate(queryClient)
192
+ expect(dehydrated.queries.length).toBe(1)
193
+ expect(dehydrated.queries[0]?.state.error).toStrictEqual(new Error('error'))
194
+ const stringified = JSON.stringify(dehydrated)
195
+ const parsed = JSON.parse(stringified)
196
+ const hydrationCache = new QueryCache()
197
+ const hydrationClient = createQueryClient({
198
+ queryCache: hydrationCache,
199
+ defaultOptions: { hydrate: { queries: { retry: 10 } } },
200
+ })
201
+ hydrate(hydrationClient, parsed, {
202
+ defaultOptions: { queries: { gcTime: 10 } },
203
+ })
204
+ expect(hydrationCache.find({ queryKey: ['string'] })?.options.retry).toBe(
205
+ 10,
206
+ )
207
+ expect(hydrationCache.find({ queryKey: ['string'] })?.options.gcTime).toBe(
208
+ 10,
209
+ )
210
+ queryClient.clear()
211
+ hydrationClient.clear()
212
+ })
213
+
214
+ test('should respect mutation defaultOptions specified on the QueryClient', async () => {
215
+ const mutationCache = new MutationCache()
216
+ const queryClient = createQueryClient({
217
+ mutationCache,
218
+ defaultOptions: {
219
+ dehydrate: {
220
+ shouldDehydrateMutation: (mutation) => mutation.state.data === 'done',
221
+ },
222
+ },
223
+ })
224
+ await executeMutation(
225
+ queryClient,
226
+ {
227
+ mutationKey: ['string'],
228
+ mutationFn: () => Promise.resolve('done'),
229
+ },
230
+ undefined,
231
+ )
232
+
233
+ const dehydrated = dehydrate(queryClient)
234
+ expect(dehydrated.mutations.length).toBe(1)
235
+ expect(dehydrated.mutations[0]?.state.data).toBe('done')
236
+ const stringified = JSON.stringify(dehydrated)
237
+ const parsed = JSON.parse(stringified)
238
+ const hydrationCache = new MutationCache()
239
+ const hydrationClient = createQueryClient({
240
+ mutationCache: hydrationCache,
241
+ defaultOptions: { hydrate: { mutations: { retry: 10 } } },
242
+ })
243
+ hydrate(hydrationClient, parsed, {
244
+ defaultOptions: { mutations: { gcTime: 10 } },
245
+ })
246
+ expect(
247
+ hydrationCache.find({ mutationKey: ['string'] })?.options.retry,
248
+ ).toBe(10)
249
+ expect(
250
+ hydrationCache.find({ mutationKey: ['string'] })?.options.gcTime,
251
+ ).toBe(10)
252
+ queryClient.clear()
253
+ hydrationClient.clear()
254
+ })
255
+
177
256
  test('should work with complex keys', async () => {
178
257
  const queryCache = new QueryCache()
179
258
  const queryClient = createQueryClient({ queryCache })
@@ -738,4 +817,95 @@ describe('dehydration and rehydration', () => {
738
817
 
739
818
  onlineMock.mockRestore()
740
819
  })
820
+
821
+ test('should dehydrate promises for pending queries', async () => {
822
+ const queryCache = new QueryCache()
823
+ const queryClient = createQueryClient({
824
+ queryCache,
825
+ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } },
826
+ })
827
+ await queryClient.prefetchQuery({
828
+ queryKey: ['success'],
829
+ queryFn: () => fetchData('success'),
830
+ })
831
+
832
+ const promise = queryClient.prefetchQuery({
833
+ queryKey: ['pending'],
834
+ queryFn: () => fetchData('pending', 10),
835
+ })
836
+ const dehydrated = dehydrate(queryClient)
837
+
838
+ expect(dehydrated.queries[0]?.promise).toBeUndefined()
839
+ expect(dehydrated.queries[1]?.promise).toBeInstanceOf(Promise)
840
+
841
+ await promise
842
+ queryClient.clear()
843
+ })
844
+
845
+ test('should hydrate promises even without observers', async () => {
846
+ const queryCache = new QueryCache()
847
+ const queryClient = createQueryClient({
848
+ queryCache,
849
+ defaultOptions: { dehydrate: { shouldDehydrateQuery: () => true } },
850
+ })
851
+ await queryClient.prefetchQuery({
852
+ queryKey: ['success'],
853
+ queryFn: () => fetchData('success'),
854
+ })
855
+
856
+ void queryClient.prefetchQuery({
857
+ queryKey: ['pending'],
858
+ queryFn: () => fetchData('pending', 20),
859
+ })
860
+ const dehydrated = dehydrate(queryClient)
861
+ // no stringify/parse here because promises can't be serialized to json
862
+ // but nextJs still can do it
863
+
864
+ const hydrationCache = new QueryCache()
865
+ const hydrationClient = createQueryClient({
866
+ queryCache: hydrationCache,
867
+ })
868
+
869
+ hydrate(hydrationClient, dehydrated)
870
+
871
+ expect(hydrationCache.find({ queryKey: ['success'] })?.state.data).toBe(
872
+ 'success',
873
+ )
874
+
875
+ expect(hydrationCache.find({ queryKey: ['pending'] })?.state).toMatchObject(
876
+ {
877
+ data: undefined,
878
+ dataUpdateCount: 0,
879
+ dataUpdatedAt: 0,
880
+ error: null,
881
+ errorUpdateCount: 0,
882
+ errorUpdatedAt: 0,
883
+ fetchFailureCount: 0,
884
+ fetchFailureReason: null,
885
+ fetchMeta: null,
886
+ fetchStatus: 'fetching',
887
+ isInvalidated: false,
888
+ status: 'pending',
889
+ },
890
+ )
891
+
892
+ await waitFor(() =>
893
+ expect(
894
+ hydrationCache.find({ queryKey: ['pending'] })?.state,
895
+ ).toMatchObject({
896
+ data: 'pending',
897
+ dataUpdateCount: 1,
898
+ dataUpdatedAt: expect.any(Number),
899
+ error: null,
900
+ errorUpdateCount: 0,
901
+ errorUpdatedAt: 0,
902
+ fetchFailureCount: 0,
903
+ fetchFailureReason: null,
904
+ fetchMeta: null,
905
+ fetchStatus: 'idle',
906
+ isInvalidated: false,
907
+ status: 'success',
908
+ }),
909
+ )
910
+ })
741
911
  })
@@ -910,4 +910,30 @@ describe('queryObserver', () => {
910
910
  const result = observer.getCurrentResult()
911
911
  expect(result.isStale).toBe(false)
912
912
  })
913
+
914
+ test('should allow staleTime as a function', async () => {
915
+ const key = queryKey()
916
+ const observer = new QueryObserver(queryClient, {
917
+ queryKey: key,
918
+ queryFn: async () => {
919
+ await sleep(5)
920
+ return {
921
+ data: 'data',
922
+ staleTime: 20,
923
+ }
924
+ },
925
+ staleTime: (query) => query.state.data?.staleTime ?? 0,
926
+ })
927
+ const results: Array<QueryObserverResult<unknown>> = []
928
+ const unsubscribe = observer.subscribe((x) => {
929
+ if (x.data) {
930
+ results.push(x)
931
+ }
932
+ })
933
+
934
+ await waitFor(() => expect(results[0]?.isStale).toBe(false))
935
+ await waitFor(() => expect(results[1]?.isStale).toBe(true))
936
+
937
+ unsubscribe()
938
+ })
913
939
  })
package/src/hydration.ts CHANGED
@@ -37,6 +37,7 @@ interface DehydratedQuery {
37
37
  queryHash: string
38
38
  queryKey: QueryKey
39
39
  state: QueryState
40
+ promise?: Promise<unknown>
40
41
  meta?: QueryMeta
41
42
  }
42
43
 
@@ -65,6 +66,16 @@ function dehydrateQuery(query: Query): DehydratedQuery {
65
66
  state: query.state,
66
67
  queryKey: query.queryKey,
67
68
  queryHash: query.queryHash,
69
+ ...(query.state.status === 'pending' && {
70
+ promise: query.promise?.catch((error) => {
71
+ if (process.env.NODE_ENV !== 'production') {
72
+ console.error(
73
+ `A query that was dehydrated as pending ended up rejecting. [${query.queryHash}]: ${error}; The error will be redacted in production builds`,
74
+ )
75
+ }
76
+ return Promise.reject(new Error('redacted'))
77
+ }),
78
+ }),
68
79
  ...(query.meta && { meta: query.meta }),
69
80
  }
70
81
  }
@@ -82,7 +93,9 @@ export function dehydrate(
82
93
  options: DehydrateOptions = {},
83
94
  ): DehydratedState {
84
95
  const filterMutation =
85
- options.shouldDehydrateMutation ?? defaultShouldDehydrateMutation
96
+ options.shouldDehydrateMutation ??
97
+ client.getDefaultOptions().dehydrate?.shouldDehydrateMutation ??
98
+ defaultShouldDehydrateMutation
86
99
 
87
100
  const mutations = client
88
101
  .getMutationCache()
@@ -92,7 +105,9 @@ export function dehydrate(
92
105
  )
93
106
 
94
107
  const filterQuery =
95
- options.shouldDehydrateQuery ?? defaultShouldDehydrateQuery
108
+ options.shouldDehydrateQuery ??
109
+ client.getDefaultOptions().dehydrate?.shouldDehydrateQuery ??
110
+ defaultShouldDehydrateQuery
96
111
 
97
112
  const queries = client
98
113
  .getQueryCache()
@@ -123,6 +138,7 @@ export function hydrate(
123
138
  mutationCache.build(
124
139
  client,
125
140
  {
141
+ ...client.getDefaultOptions().hydrate?.mutations,
126
142
  ...options?.defaultOptions?.mutations,
127
143
  ...mutationOptions,
128
144
  },
@@ -130,8 +146,8 @@ export function hydrate(
130
146
  )
131
147
  })
132
148
 
133
- queries.forEach(({ queryKey, state, queryHash, meta }) => {
134
- const query = queryCache.get(queryHash)
149
+ queries.forEach(({ queryKey, state, queryHash, meta, promise }) => {
150
+ let query = queryCache.get(queryHash)
135
151
 
136
152
  // Do not hydrate if an existing query exists with newer data
137
153
  if (query) {
@@ -141,24 +157,30 @@ export function hydrate(
141
157
  const { fetchStatus: _ignored, ...dehydratedQueryState } = state
142
158
  query.setState(dehydratedQueryState)
143
159
  }
144
- return
160
+ } else {
161
+ // Restore query
162
+ query = queryCache.build(
163
+ client,
164
+ {
165
+ ...client.getDefaultOptions().hydrate?.queries,
166
+ ...options?.defaultOptions?.queries,
167
+ queryKey,
168
+ queryHash,
169
+ meta,
170
+ },
171
+ // Reset fetch status to idle to avoid
172
+ // query being stuck in fetching state upon hydration
173
+ {
174
+ ...state,
175
+ fetchStatus: 'idle',
176
+ },
177
+ )
145
178
  }
146
179
 
147
- // Restore query
148
- queryCache.build(
149
- client,
150
- {
151
- ...options?.defaultOptions?.queries,
152
- queryKey,
153
- queryHash,
154
- meta,
155
- },
156
- // Reset fetch status to idle to avoid
157
- // query being stuck in fetching state upon hydration
158
- {
159
- ...state,
160
- fetchStatus: 'idle',
161
- },
162
- )
180
+ if (promise) {
181
+ // this doesn't actually fetch - it just creates a retryer
182
+ // which will re-use the passed `initialPromise`
183
+ void query.fetch(undefined, { initialPromise: promise })
184
+ }
163
185
  })
164
186
  }
@@ -1,4 +1,4 @@
1
- import { addToEnd, addToStart, skipToken } from './utils'
1
+ import { addToEnd, addToStart, ensureQueryFn } from './utils'
2
2
  import type { QueryBehavior } from './query'
3
3
  import type {
4
4
  InfiniteData,
@@ -37,22 +37,7 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
37
37
  })
38
38
  }
39
39
 
40
- // Get query function
41
- const queryFn =
42
- context.options.queryFn && context.options.queryFn !== skipToken
43
- ? context.options.queryFn
44
- : () => {
45
- if (process.env.NODE_ENV !== 'production') {
46
- if (context.options.queryFn === skipToken) {
47
- console.error(
48
- `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${context.options.queryHash}'`,
49
- )
50
- }
51
- }
52
- return Promise.reject(
53
- new Error(`Missing queryFn: '${context.options.queryHash}'`),
54
- )
55
- }
40
+ const queryFn = ensureQueryFn(context.options, context.fetchOptions)
56
41
 
57
42
  // Create function to fetch a page
58
43
  const fetchPage = async (
package/src/query.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { noop, replaceData, skipToken, timeUntilStale } from './utils'
1
+ import { ensureQueryFn, noop, replaceData, timeUntilStale } from './utils'
2
2
  import { notifyManager } from './notifyManager'
3
3
  import { canFetch, createRetryer, isCancelledError } from './retryer'
4
4
  import { Removable } from './removable'
@@ -8,6 +8,7 @@ import type {
8
8
  FetchStatus,
9
9
  InitialDataFunction,
10
10
  OmitKeyof,
11
+ QueryFunction,
11
12
  QueryFunctionContext,
12
13
  QueryKey,
13
14
  QueryMeta,
@@ -82,9 +83,10 @@ export interface FetchMeta {
82
83
  fetchMore?: { direction: FetchDirection }
83
84
  }
84
85
 
85
- export interface FetchOptions {
86
+ export interface FetchOptions<TData = unknown> {
86
87
  cancelRefetch?: boolean
87
88
  meta?: FetchMeta
89
+ initialPromise?: Promise<TData>
88
90
  }
89
91
 
90
92
  interface FailedAction<TError> {
@@ -182,6 +184,10 @@ export class Query<
182
184
  return this.options.meta
183
185
  }
184
186
 
187
+ get promise(): Promise<TData> | undefined {
188
+ return this.#retryer?.promise
189
+ }
190
+
185
191
  setOptions(
186
192
  options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
187
193
  ): void {
@@ -330,7 +336,7 @@ export class Query<
330
336
 
331
337
  fetch(
332
338
  options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
333
- fetchOptions?: FetchOptions,
339
+ fetchOptions?: FetchOptions<TQueryFnData>,
334
340
  ): Promise<TData> {
335
341
  if (this.state.fetchStatus !== 'idle') {
336
342
  if (this.state.data !== undefined && fetchOptions?.cancelRefetch) {
@@ -368,15 +374,6 @@ export class Query<
368
374
 
369
375
  const abortController = new AbortController()
370
376
 
371
- // Create query function context
372
- const queryFnContext: OmitKeyof<
373
- QueryFunctionContext<TQueryKey>,
374
- 'signal'
375
- > = {
376
- queryKey: this.queryKey,
377
- meta: this.meta,
378
- }
379
-
380
377
  // Adds an enumerable signal property to the object that
381
378
  // which sets abortSignalConsumed to true when the signal
382
379
  // is read.
@@ -390,36 +387,31 @@ export class Query<
390
387
  })
391
388
  }
392
389
 
393
- addSignalProperty(queryFnContext)
394
-
395
390
  // Create fetch function
396
391
  const fetchFn = () => {
397
- if (process.env.NODE_ENV !== 'production') {
398
- if (this.options.queryFn === skipToken) {
399
- console.error(
400
- `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${this.options.queryHash}'`,
401
- )
402
- }
392
+ const queryFn = ensureQueryFn(this.options, fetchOptions)
393
+
394
+ // Create query function context
395
+ const queryFnContext: OmitKeyof<
396
+ QueryFunctionContext<TQueryKey>,
397
+ 'signal'
398
+ > = {
399
+ queryKey: this.queryKey,
400
+ meta: this.meta,
403
401
  }
404
402
 
405
- if (!this.options.queryFn || this.options.queryFn === skipToken) {
406
- return Promise.reject(
407
- new Error(`Missing queryFn: '${this.options.queryHash}'`),
408
- )
409
- }
403
+ addSignalProperty(queryFnContext)
410
404
 
411
405
  this.#abortSignalConsumed = false
412
406
  if (this.options.persister) {
413
407
  return this.options.persister(
414
- this.options.queryFn,
408
+ queryFn as QueryFunction<any>,
415
409
  queryFnContext as QueryFunctionContext<TQueryKey>,
416
410
  this as unknown as Query,
417
411
  )
418
412
  }
419
413
 
420
- return this.options.queryFn(
421
- queryFnContext as QueryFunctionContext<TQueryKey>,
422
- )
414
+ return queryFn(queryFnContext as QueryFunctionContext<TQueryKey>)
423
415
  }
424
416
 
425
417
  // Trigger behavior hook
@@ -483,6 +475,9 @@ export class Query<
483
475
 
484
476
  // Try to fetch the data
485
477
  this.#retryer = createRetryer({
478
+ initialPromise: fetchOptions?.initialPromise as
479
+ | Promise<TData>
480
+ | undefined,
486
481
  fn: context.fetchFn as () => Promise<TData>,
487
482
  abort: abortController.abort.bind(abortController),
488
483
  onSuccess: (data) => {
package/src/queryCache.ts CHANGED
@@ -97,7 +97,12 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
97
97
  this.#queries = new Map<string, Query>()
98
98
  }
99
99
 
100
- build<TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
100
+ build<
101
+ TQueryFnData = unknown,
102
+ TError = DefaultError,
103
+ TData = TQueryFnData,
104
+ TQueryKey extends QueryKey = QueryKey,
105
+ >(
101
106
  client: QueryClient,
102
107
  options: WithRequired<
103
108
  QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
@@ -4,6 +4,7 @@ import {
4
4
  hashQueryKeyByOptions,
5
5
  noop,
6
6
  partialMatchKey,
7
+ resolveStaleTime,
7
8
  skipToken,
8
9
  } from './utils'
9
10
  import { QueryCache } from './queryCache'
@@ -142,7 +143,7 @@ export class QueryClient {
142
143
 
143
144
  if (
144
145
  options.revalidateIfStale &&
145
- query.isStaleByTime(defaultedOptions.staleTime)
146
+ query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query))
146
147
  ) {
147
148
  void this.prefetchQuery(defaultedOptions)
148
149
  }
@@ -343,7 +344,9 @@ export class QueryClient {
343
344
 
344
345
  const query = this.#queryCache.build(this, defaultedOptions)
345
346
 
346
- return query.isStaleByTime(defaultedOptions.staleTime)
347
+ return query.isStaleByTime(
348
+ resolveStaleTime(defaultedOptions.staleTime, query),
349
+ )
347
350
  ? query.fetch(defaultedOptions)
348
351
  : Promise.resolve(query.state.data as TData)
349
352
  }
@@ -3,6 +3,7 @@ import {
3
3
  isValidTimeout,
4
4
  noop,
5
5
  replaceData,
6
+ resolveStaleTime,
6
7
  shallowEqualObjects,
7
8
  timeUntilStale,
8
9
  } from './utils'
@@ -190,7 +191,8 @@ export class QueryObserver<
190
191
  mounted &&
191
192
  (this.#currentQuery !== prevQuery ||
192
193
  this.options.enabled !== prevOptions.enabled ||
193
- this.options.staleTime !== prevOptions.staleTime)
194
+ resolveStaleTime(this.options.staleTime, this.#currentQuery) !==
195
+ resolveStaleTime(prevOptions.staleTime, this.#currentQuery))
194
196
  ) {
195
197
  this.#updateStaleTimeout()
196
198
  }
@@ -318,7 +320,7 @@ export class QueryObserver<
318
320
  }
319
321
 
320
322
  #executeFetch(
321
- fetchOptions?: ObserverFetchOptions,
323
+ fetchOptions?: Omit<ObserverFetchOptions, 'initialPromise'>,
322
324
  ): Promise<TQueryData | undefined> {
323
325
  // Make sure we reference the latest query as the current one might have been removed
324
326
  this.#updateQuery()
@@ -338,19 +340,16 @@ export class QueryObserver<
338
340
 
339
341
  #updateStaleTimeout(): void {
340
342
  this.#clearStaleTimeout()
343
+ const staleTime = resolveStaleTime(
344
+ this.options.staleTime,
345
+ this.#currentQuery,
346
+ )
341
347
 
342
- if (
343
- isServer ||
344
- this.#currentResult.isStale ||
345
- !isValidTimeout(this.options.staleTime)
346
- ) {
348
+ if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) {
347
349
  return
348
350
  }
349
351
 
350
- const time = timeUntilStale(
351
- this.#currentResult.dataUpdatedAt,
352
- this.options.staleTime,
353
- )
352
+ const time = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime)
354
353
 
355
354
  // The timeout is sometimes triggered 1 ms before the stale time expiration.
356
355
  // To mitigate this issue we always add 1 ms to the timeout.
@@ -742,7 +741,10 @@ function isStale(
742
741
  query: Query<any, any, any, any>,
743
742
  options: QueryObserverOptions<any, any, any, any, any>,
744
743
  ): boolean {
745
- return options.enabled !== false && query.isStaleByTime(options.staleTime)
744
+ return (
745
+ options.enabled !== false &&
746
+ query.isStaleByTime(resolveStaleTime(options.staleTime, query))
747
+ )
746
748
  }
747
749
 
748
750
  // this function would decide if we will update the observer's 'current'
package/src/retryer.ts CHANGED
@@ -7,6 +7,7 @@ import type { CancelOptions, DefaultError, NetworkMode } from './types'
7
7
 
8
8
  interface RetryerConfig<TData = unknown, TError = DefaultError> {
9
9
  fn: () => TData | Promise<TData>
10
+ initialPromise?: Promise<TData>
10
11
  abort?: () => void
11
12
  onError?: (error: TError) => void
12
13
  onSuccess?: (data: TData) => void
@@ -146,9 +147,13 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
146
147
 
147
148
  let promiseOrValue: any
148
149
 
150
+ // we can re-use config.initialPromise on the first call of run()
151
+ const initialPromise =
152
+ failureCount === 0 ? config.initialPromise : undefined
153
+
149
154
  // Execute query
150
155
  try {
151
- promiseOrValue = config.fn()
156
+ promiseOrValue = initialPromise ?? config.fn()
152
157
  } catch (error) {
153
158
  promiseOrValue = Promise.reject(error)
154
159
  }
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /* istanbul ignore file */
2
2
 
3
+ import type { DehydrateOptions, HydrateOptions } from './hydration'
3
4
  import type { MutationState } from './mutation'
4
5
  import type { FetchDirection, Query, QueryBehavior } from './query'
5
6
  import type { RetryDelayValue, RetryValue } from './retryer'
@@ -46,6 +47,13 @@ export type QueryFunction<
46
47
  TPageParam = never,
47
48
  > = (context: QueryFunctionContext<TQueryKey, TPageParam>) => T | Promise<T>
48
49
 
50
+ export type StaleTime<
51
+ TQueryFnData = unknown,
52
+ TError = DefaultError,
53
+ TData = TQueryFnData,
54
+ TQueryKey extends QueryKey = QueryKey,
55
+ > = number | ((query: Query<TQueryFnData, TError, TData, TQueryKey>) => number)
56
+
49
57
  export type QueryPersister<
50
58
  T = unknown,
51
59
  TQueryKey extends QueryKey = QueryKey,
@@ -253,8 +261,9 @@ export interface QueryObserverOptions<
253
261
  /**
254
262
  * The time in milliseconds after data is considered stale.
255
263
  * If set to `Infinity`, the data will never be considered stale.
264
+ * If set to a function, the function will be executed with the query to compute a `staleTime`.
256
265
  */
257
- staleTime?: number
266
+ staleTime?: StaleTime<TQueryFnData, TError, TQueryData, TQueryKey>
258
267
  /**
259
268
  * If set to a number, the query will continuously refetch at this frequency in milliseconds.
260
269
  * If set to a function, the function will be executed with the latest data and query to compute a frequency
@@ -426,7 +435,7 @@ export interface FetchQueryOptions<
426
435
  * The time in milliseconds after data is considered stale.
427
436
  * If the data is fresh it will be returned from the cache.
428
437
  */
429
- staleTime?: number
438
+ staleTime?: StaleTime<TQueryFnData, TError, TData, TQueryKey>
430
439
  }
431
440
 
432
441
  export interface EnsureQueryDataOptions<
@@ -1119,6 +1128,8 @@ export interface DefaultOptions<TError = DefaultError> {
1119
1128
  'suspense' | 'queryKey'
1120
1129
  >
1121
1130
  mutations?: MutationObserverOptions<unknown, TError, unknown, unknown>
1131
+ hydrate?: HydrateOptions['defaultOptions']
1132
+ dehydrate?: DehydrateOptions
1122
1133
  }
1123
1134
 
1124
1135
  export interface CancelOptions {