@tanstack/query-core 5.38.0 → 5.40.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 (119) hide show
  1. package/build/legacy/{types-BvcshvE9.d.cts → hydration-DAs3cQH5.d.cts} +44 -3
  2. package/build/legacy/{types-BtrVwz9w.d.ts → hydration-yBB_smkL.d.ts} +44 -3
  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.d.cts +1 -1
  34. package/build/legacy/queryCache.d.ts +1 -1
  35. package/build/legacy/queryClient.d.cts +1 -1
  36. package/build/legacy/queryClient.d.ts +1 -1
  37. package/build/legacy/queryObserver.cjs.map +1 -1
  38. package/build/legacy/queryObserver.d.cts +1 -1
  39. package/build/legacy/queryObserver.d.ts +1 -1
  40. package/build/legacy/queryObserver.js.map +1 -1
  41. package/build/legacy/retryer.cjs +2 -1
  42. package/build/legacy/retryer.cjs.map +1 -1
  43. package/build/legacy/retryer.d.cts +1 -1
  44. package/build/legacy/retryer.d.ts +1 -1
  45. package/build/legacy/retryer.js +2 -1
  46. package/build/legacy/retryer.js.map +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 +18 -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 +17 -0
  55. package/build/legacy/utils.js.map +1 -1
  56. package/build/modern/{types-BvcshvE9.d.cts → hydration-DAs3cQH5.d.cts} +44 -3
  57. package/build/modern/{types-BtrVwz9w.d.ts → hydration-yBB_smkL.d.ts} +44 -3
  58. package/build/modern/hydration.cjs +35 -20
  59. package/build/modern/hydration.cjs.map +1 -1
  60. package/build/modern/hydration.d.cts +1 -34
  61. package/build/modern/hydration.d.ts +1 -34
  62. package/build/modern/hydration.js +35 -20
  63. package/build/modern/hydration.js.map +1 -1
  64. package/build/modern/index.d.cts +1 -2
  65. package/build/modern/index.d.ts +1 -2
  66. package/build/modern/infiniteQueryBehavior.cjs +1 -12
  67. package/build/modern/infiniteQueryBehavior.cjs.map +1 -1
  68. package/build/modern/infiniteQueryBehavior.d.cts +1 -1
  69. package/build/modern/infiniteQueryBehavior.d.ts +1 -1
  70. package/build/modern/infiniteQueryBehavior.js +2 -13
  71. package/build/modern/infiniteQueryBehavior.js.map +1 -1
  72. package/build/modern/infiniteQueryObserver.d.cts +1 -1
  73. package/build/modern/infiniteQueryObserver.d.ts +1 -1
  74. package/build/modern/mutation.d.cts +1 -1
  75. package/build/modern/mutation.d.ts +1 -1
  76. package/build/modern/mutationCache.d.cts +1 -1
  77. package/build/modern/mutationCache.d.ts +1 -1
  78. package/build/modern/mutationObserver.d.cts +1 -1
  79. package/build/modern/mutationObserver.d.ts +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 +12 -21
  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 +13 -22
  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.d.cts +1 -1
  91. package/build/modern/queryClient.d.ts +1 -1
  92. package/build/modern/queryObserver.cjs.map +1 -1
  93. package/build/modern/queryObserver.d.cts +1 -1
  94. package/build/modern/queryObserver.d.ts +1 -1
  95. package/build/modern/queryObserver.js.map +1 -1
  96. package/build/modern/retryer.cjs +2 -1
  97. package/build/modern/retryer.cjs.map +1 -1
  98. package/build/modern/retryer.d.cts +1 -1
  99. package/build/modern/retryer.d.ts +1 -1
  100. package/build/modern/retryer.js +2 -1
  101. package/build/modern/retryer.js.map +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 +18 -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 +17 -0
  110. package/build/modern/utils.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/__tests__/hydration.test.tsx +170 -0
  113. package/src/hydration.ts +43 -21
  114. package/src/infiniteQueryBehavior.ts +2 -17
  115. package/src/query.ts +24 -29
  116. package/src/queryObserver.ts +1 -1
  117. package/src/retryer.ts +6 -1
  118. package/src/types.ts +3 -0
  119. package/src/utils.ts +36 -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
  })
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) => {
@@ -318,7 +318,7 @@ export class QueryObserver<
318
318
  }
319
319
 
320
320
  #executeFetch(
321
- fetchOptions?: ObserverFetchOptions,
321
+ fetchOptions?: Omit<ObserverFetchOptions, 'initialPromise'>,
322
322
  ): Promise<TQueryData | undefined> {
323
323
  // Make sure we reference the latest query as the current one might have been removed
324
324
  this.#updateQuery()
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'
@@ -1119,6 +1120,8 @@ export interface DefaultOptions<TError = DefaultError> {
1119
1120
  'suspense' | 'queryKey'
1120
1121
  >
1121
1122
  mutations?: MutationObserverOptions<unknown, TError, unknown, unknown>
1123
+ hydrate?: HydrateOptions['defaultOptions']
1124
+ dehydrate?: DehydrateOptions
1122
1125
  }
1123
1126
 
1124
1127
  export interface CancelOptions {
package/src/utils.ts CHANGED
@@ -1,12 +1,13 @@
1
- import type { Mutation } from './mutation'
2
- import type { Query } from './query'
3
1
  import type {
4
2
  FetchStatus,
5
3
  MutationKey,
6
4
  MutationStatus,
5
+ QueryFunction,
7
6
  QueryKey,
8
7
  QueryOptions,
9
8
  } from './types'
9
+ import type { Mutation } from './mutation'
10
+ import type { FetchOptions, Query } from './query'
10
11
 
11
12
  // TYPES
12
13
 
@@ -349,3 +350,36 @@ export function addToStart<T>(items: Array<T>, item: T, max = 0): Array<T> {
349
350
 
350
351
  export const skipToken = Symbol()
351
352
  export type SkipToken = typeof skipToken
353
+
354
+ export const ensureQueryFn = <
355
+ TQueryFnData = unknown,
356
+ TQueryKey extends QueryKey = QueryKey,
357
+ >(
358
+ options: {
359
+ queryFn?: QueryFunction<TQueryFnData, TQueryKey> | SkipToken
360
+ queryHash?: string
361
+ },
362
+ fetchOptions?: FetchOptions<TQueryFnData>,
363
+ ): QueryFunction<TQueryFnData, TQueryKey> => {
364
+ if (process.env.NODE_ENV !== 'production') {
365
+ if (options.queryFn === skipToken) {
366
+ console.error(
367
+ `Attempted to invoke queryFn when set to skipToken. This is likely a configuration error. Query hash: '${options.queryHash}'`,
368
+ )
369
+ }
370
+ }
371
+
372
+ // if we attempt to retry a fetch that was triggered from an initialPromise
373
+ // when we don't have a queryFn yet, we can't retry, so we just return the already rejected initialPromise
374
+ // if an observer has already mounted, we will be able to retry with that queryFn
375
+ if (!options.queryFn && fetchOptions?.initialPromise) {
376
+ return () => fetchOptions.initialPromise!
377
+ }
378
+
379
+ if (!options.queryFn || options.queryFn === skipToken) {
380
+ return () =>
381
+ Promise.reject(new Error(`Missing queryFn: '${options.queryHash}'`))
382
+ }
383
+
384
+ return options.queryFn
385
+ }