@tanstack/query-core 4.0.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 (85) hide show
  1. package/build/cjs/focusManager.js +101 -0
  2. package/build/cjs/focusManager.js.map +1 -0
  3. package/build/cjs/hydration.js +112 -0
  4. package/build/cjs/hydration.js.map +1 -0
  5. package/build/cjs/index.js +51 -0
  6. package/build/cjs/index.js.map +1 -0
  7. package/build/cjs/infiniteQueryBehavior.js +160 -0
  8. package/build/cjs/infiniteQueryBehavior.js.map +1 -0
  9. package/build/cjs/infiniteQueryObserver.js +92 -0
  10. package/build/cjs/infiniteQueryObserver.js.map +1 -0
  11. package/build/cjs/logger.js +18 -0
  12. package/build/cjs/logger.js.map +1 -0
  13. package/build/cjs/mutation.js +258 -0
  14. package/build/cjs/mutation.js.map +1 -0
  15. package/build/cjs/mutationCache.js +99 -0
  16. package/build/cjs/mutationCache.js.map +1 -0
  17. package/build/cjs/mutationObserver.js +130 -0
  18. package/build/cjs/mutationObserver.js.map +1 -0
  19. package/build/cjs/notifyManager.js +114 -0
  20. package/build/cjs/notifyManager.js.map +1 -0
  21. package/build/cjs/onlineManager.js +100 -0
  22. package/build/cjs/onlineManager.js.map +1 -0
  23. package/build/cjs/queriesObserver.js +170 -0
  24. package/build/cjs/queriesObserver.js.map +1 -0
  25. package/build/cjs/query.js +474 -0
  26. package/build/cjs/query.js.map +1 -0
  27. package/build/cjs/queryCache.js +140 -0
  28. package/build/cjs/queryCache.js.map +1 -0
  29. package/build/cjs/queryClient.js +357 -0
  30. package/build/cjs/queryClient.js.map +1 -0
  31. package/build/cjs/queryObserver.js +521 -0
  32. package/build/cjs/queryObserver.js.map +1 -0
  33. package/build/cjs/removable.js +47 -0
  34. package/build/cjs/removable.js.map +1 -0
  35. package/build/cjs/retryer.js +177 -0
  36. package/build/cjs/retryer.js.map +1 -0
  37. package/build/cjs/subscribable.js +43 -0
  38. package/build/cjs/subscribable.js.map +1 -0
  39. package/build/cjs/utils.js +356 -0
  40. package/build/cjs/utils.js.map +1 -0
  41. package/build/esm/index.js +3077 -0
  42. package/build/esm/index.js.map +1 -0
  43. package/build/stats-html.html +2689 -0
  44. package/build/umd/index.development.js +3106 -0
  45. package/build/umd/index.development.js.map +1 -0
  46. package/build/umd/index.production.js +12 -0
  47. package/build/umd/index.production.js.map +1 -0
  48. package/package.json +25 -0
  49. package/src/focusManager.ts +89 -0
  50. package/src/hydration.ts +164 -0
  51. package/src/index.ts +35 -0
  52. package/src/infiniteQueryBehavior.ts +214 -0
  53. package/src/infiniteQueryObserver.ts +159 -0
  54. package/src/logger.native.ts +11 -0
  55. package/src/logger.ts +9 -0
  56. package/src/mutation.ts +349 -0
  57. package/src/mutationCache.ts +157 -0
  58. package/src/mutationObserver.ts +195 -0
  59. package/src/notifyManager.ts +96 -0
  60. package/src/onlineManager.ts +89 -0
  61. package/src/queriesObserver.ts +211 -0
  62. package/src/query.ts +612 -0
  63. package/src/queryCache.ts +206 -0
  64. package/src/queryClient.ts +716 -0
  65. package/src/queryObserver.ts +748 -0
  66. package/src/removable.ts +37 -0
  67. package/src/retryer.ts +215 -0
  68. package/src/subscribable.ts +33 -0
  69. package/src/tests/focusManager.test.tsx +155 -0
  70. package/src/tests/hydration.test.tsx +429 -0
  71. package/src/tests/infiniteQueryBehavior.test.tsx +124 -0
  72. package/src/tests/infiniteQueryObserver.test.tsx +64 -0
  73. package/src/tests/mutationCache.test.tsx +260 -0
  74. package/src/tests/mutationObserver.test.tsx +75 -0
  75. package/src/tests/mutations.test.tsx +363 -0
  76. package/src/tests/notifyManager.test.tsx +51 -0
  77. package/src/tests/onlineManager.test.tsx +148 -0
  78. package/src/tests/queriesObserver.test.tsx +330 -0
  79. package/src/tests/query.test.tsx +888 -0
  80. package/src/tests/queryCache.test.tsx +236 -0
  81. package/src/tests/queryClient.test.tsx +1435 -0
  82. package/src/tests/queryObserver.test.tsx +802 -0
  83. package/src/tests/utils.test.tsx +360 -0
  84. package/src/types.ts +705 -0
  85. package/src/utils.ts +435 -0
@@ -0,0 +1,748 @@
1
+ import { DefaultedQueryObserverOptions, RefetchPageFilters } from './types'
2
+ import {
3
+ isServer,
4
+ isValidTimeout,
5
+ noop,
6
+ replaceData,
7
+ shallowEqualObjects,
8
+ timeUntilStale,
9
+ } from './utils'
10
+ import { notifyManager } from './notifyManager'
11
+ import type {
12
+ PlaceholderDataFunction,
13
+ QueryKey,
14
+ QueryObserverBaseResult,
15
+ QueryObserverOptions,
16
+ QueryObserverResult,
17
+ QueryOptions,
18
+ RefetchOptions,
19
+ } from './types'
20
+ import type { Query, QueryState, Action, FetchOptions } from './query'
21
+ import type { QueryClient } from './queryClient'
22
+ import { focusManager } from './focusManager'
23
+ import { Subscribable } from './subscribable'
24
+ import { canFetch, isCancelledError } from './retryer'
25
+
26
+ type QueryObserverListener<TData, TError> = (
27
+ result: QueryObserverResult<TData, TError>,
28
+ ) => void
29
+
30
+ export interface NotifyOptions {
31
+ cache?: boolean
32
+ listeners?: boolean
33
+ onError?: boolean
34
+ onSuccess?: boolean
35
+ }
36
+
37
+ export interface ObserverFetchOptions extends FetchOptions {
38
+ throwOnError?: boolean
39
+ }
40
+
41
+ export class QueryObserver<
42
+ TQueryFnData = unknown,
43
+ TError = unknown,
44
+ TData = TQueryFnData,
45
+ TQueryData = TQueryFnData,
46
+ TQueryKey extends QueryKey = QueryKey,
47
+ > extends Subscribable<QueryObserverListener<TData, TError>> {
48
+ options: QueryObserverOptions<
49
+ TQueryFnData,
50
+ TError,
51
+ TData,
52
+ TQueryData,
53
+ TQueryKey
54
+ >
55
+
56
+ private client: QueryClient
57
+ private currentQuery!: Query<TQueryFnData, TError, TQueryData, TQueryKey>
58
+ private currentQueryInitialState!: QueryState<TQueryData, TError>
59
+ private currentResult!: QueryObserverResult<TData, TError>
60
+ private currentResultState?: QueryState<TQueryData, TError>
61
+ private currentResultOptions?: QueryObserverOptions<
62
+ TQueryFnData,
63
+ TError,
64
+ TData,
65
+ TQueryData,
66
+ TQueryKey
67
+ >
68
+ private previousQueryResult?: QueryObserverResult<TData, TError>
69
+ private selectError: TError | null
70
+ private selectFn?: (data: TQueryData) => TData
71
+ private selectResult?: TData
72
+ private staleTimeoutId?: ReturnType<typeof setTimeout>
73
+ private refetchIntervalId?: ReturnType<typeof setInterval>
74
+ private currentRefetchInterval?: number | false
75
+ private trackedProps!: Set<keyof QueryObserverResult>
76
+
77
+ constructor(
78
+ client: QueryClient,
79
+ options: QueryObserverOptions<
80
+ TQueryFnData,
81
+ TError,
82
+ TData,
83
+ TQueryData,
84
+ TQueryKey
85
+ >,
86
+ ) {
87
+ super()
88
+
89
+ this.client = client
90
+ this.options = options
91
+ this.trackedProps = new Set()
92
+ this.selectError = null
93
+ this.bindMethods()
94
+ this.setOptions(options)
95
+ }
96
+
97
+ protected bindMethods(): void {
98
+ this.remove = this.remove.bind(this)
99
+ this.refetch = this.refetch.bind(this)
100
+ }
101
+
102
+ protected onSubscribe(): void {
103
+ if (this.listeners.length === 1) {
104
+ this.currentQuery.addObserver(this)
105
+
106
+ if (shouldFetchOnMount(this.currentQuery, this.options)) {
107
+ this.executeFetch()
108
+ }
109
+
110
+ this.updateTimers()
111
+ }
112
+ }
113
+
114
+ protected onUnsubscribe(): void {
115
+ if (!this.listeners.length) {
116
+ this.destroy()
117
+ }
118
+ }
119
+
120
+ shouldFetchOnReconnect(): boolean {
121
+ return shouldFetchOn(
122
+ this.currentQuery,
123
+ this.options,
124
+ this.options.refetchOnReconnect,
125
+ )
126
+ }
127
+
128
+ shouldFetchOnWindowFocus(): boolean {
129
+ return shouldFetchOn(
130
+ this.currentQuery,
131
+ this.options,
132
+ this.options.refetchOnWindowFocus,
133
+ )
134
+ }
135
+
136
+ destroy(): void {
137
+ this.listeners = []
138
+ this.clearStaleTimeout()
139
+ this.clearRefetchInterval()
140
+ this.currentQuery.removeObserver(this)
141
+ }
142
+
143
+ setOptions(
144
+ options?: QueryObserverOptions<
145
+ TQueryFnData,
146
+ TError,
147
+ TData,
148
+ TQueryData,
149
+ TQueryKey
150
+ >,
151
+ notifyOptions?: NotifyOptions,
152
+ ): void {
153
+ const prevOptions = this.options
154
+ const prevQuery = this.currentQuery
155
+
156
+ this.options = this.client.defaultQueryOptions(options)
157
+
158
+ if (
159
+ typeof this.options.enabled !== 'undefined' &&
160
+ typeof this.options.enabled !== 'boolean'
161
+ ) {
162
+ throw new Error('Expected enabled to be a boolean')
163
+ }
164
+
165
+ // Keep previous query key if the user does not supply one
166
+ if (!this.options.queryKey) {
167
+ this.options.queryKey = prevOptions.queryKey
168
+ }
169
+
170
+ this.updateQuery()
171
+
172
+ const mounted = this.hasListeners()
173
+
174
+ // Fetch if there are subscribers
175
+ if (
176
+ mounted &&
177
+ shouldFetchOptionally(
178
+ this.currentQuery,
179
+ prevQuery,
180
+ this.options,
181
+ prevOptions,
182
+ )
183
+ ) {
184
+ this.executeFetch()
185
+ }
186
+
187
+ // Update result
188
+ this.updateResult(notifyOptions)
189
+
190
+ // Update stale interval if needed
191
+ if (
192
+ mounted &&
193
+ (this.currentQuery !== prevQuery ||
194
+ this.options.enabled !== prevOptions.enabled ||
195
+ this.options.staleTime !== prevOptions.staleTime)
196
+ ) {
197
+ this.updateStaleTimeout()
198
+ }
199
+
200
+ const nextRefetchInterval = this.computeRefetchInterval()
201
+
202
+ // Update refetch interval if needed
203
+ if (
204
+ mounted &&
205
+ (this.currentQuery !== prevQuery ||
206
+ this.options.enabled !== prevOptions.enabled ||
207
+ nextRefetchInterval !== this.currentRefetchInterval)
208
+ ) {
209
+ this.updateRefetchInterval(nextRefetchInterval)
210
+ }
211
+ }
212
+
213
+ getOptimisticResult(
214
+ options: DefaultedQueryObserverOptions<
215
+ TQueryFnData,
216
+ TError,
217
+ TData,
218
+ TQueryData,
219
+ TQueryKey
220
+ >,
221
+ ): QueryObserverResult<TData, TError> {
222
+ const query = this.client.getQueryCache().build(this.client, options)
223
+
224
+ return this.createResult(query, options)
225
+ }
226
+
227
+ getCurrentResult(): QueryObserverResult<TData, TError> {
228
+ return this.currentResult
229
+ }
230
+
231
+ trackResult(
232
+ result: QueryObserverResult<TData, TError>,
233
+ ): QueryObserverResult<TData, TError> {
234
+ const trackedResult = {} as QueryObserverResult<TData, TError>
235
+
236
+ Object.keys(result).forEach((key) => {
237
+ Object.defineProperty(trackedResult, key, {
238
+ configurable: false,
239
+ enumerable: true,
240
+ get: () => {
241
+ this.trackedProps.add(key as keyof QueryObserverResult)
242
+ return result[key as keyof QueryObserverResult]
243
+ },
244
+ })
245
+ })
246
+
247
+ return trackedResult
248
+ }
249
+
250
+ getCurrentQuery(): Query<TQueryFnData, TError, TQueryData, TQueryKey> {
251
+ return this.currentQuery
252
+ }
253
+
254
+ remove(): void {
255
+ this.client.getQueryCache().remove(this.currentQuery)
256
+ }
257
+
258
+ refetch<TPageData>({
259
+ refetchPage,
260
+ ...options
261
+ }: RefetchOptions & RefetchPageFilters<TPageData> = {}): Promise<
262
+ QueryObserverResult<TData, TError>
263
+ > {
264
+ return this.fetch({
265
+ ...options,
266
+ meta: { refetchPage },
267
+ })
268
+ }
269
+
270
+ fetchOptimistic(
271
+ options: QueryObserverOptions<
272
+ TQueryFnData,
273
+ TError,
274
+ TData,
275
+ TQueryData,
276
+ TQueryKey
277
+ >,
278
+ ): Promise<QueryObserverResult<TData, TError>> {
279
+ const defaultedOptions = this.client.defaultQueryOptions(options)
280
+
281
+ const query = this.client
282
+ .getQueryCache()
283
+ .build(this.client, defaultedOptions)
284
+ query.isFetchingOptimistic = true
285
+
286
+ return query.fetch().then(() => this.createResult(query, defaultedOptions))
287
+ }
288
+
289
+ protected fetch(
290
+ fetchOptions: ObserverFetchOptions,
291
+ ): Promise<QueryObserverResult<TData, TError>> {
292
+ return this.executeFetch({
293
+ ...fetchOptions,
294
+ cancelRefetch: fetchOptions.cancelRefetch ?? true,
295
+ }).then(() => {
296
+ this.updateResult()
297
+ return this.currentResult
298
+ })
299
+ }
300
+
301
+ private executeFetch(
302
+ fetchOptions?: ObserverFetchOptions,
303
+ ): Promise<TQueryData | undefined> {
304
+ // Make sure we reference the latest query as the current one might have been removed
305
+ this.updateQuery()
306
+
307
+ // Fetch
308
+ let promise: Promise<TQueryData | undefined> = this.currentQuery.fetch(
309
+ this.options as QueryOptions<TQueryFnData, TError, TQueryData, TQueryKey>,
310
+ fetchOptions,
311
+ )
312
+
313
+ if (!fetchOptions?.throwOnError) {
314
+ promise = promise.catch(noop)
315
+ }
316
+
317
+ return promise
318
+ }
319
+
320
+ private updateStaleTimeout(): void {
321
+ this.clearStaleTimeout()
322
+
323
+ if (
324
+ isServer ||
325
+ this.currentResult.isStale ||
326
+ !isValidTimeout(this.options.staleTime)
327
+ ) {
328
+ return
329
+ }
330
+
331
+ const time = timeUntilStale(
332
+ this.currentResult.dataUpdatedAt,
333
+ this.options.staleTime,
334
+ )
335
+
336
+ // The timeout is sometimes triggered 1 ms before the stale time expiration.
337
+ // To mitigate this issue we always add 1 ms to the timeout.
338
+ const timeout = time + 1
339
+
340
+ this.staleTimeoutId = setTimeout(() => {
341
+ if (!this.currentResult.isStale) {
342
+ this.updateResult()
343
+ }
344
+ }, timeout)
345
+ }
346
+
347
+ private computeRefetchInterval() {
348
+ return typeof this.options.refetchInterval === 'function'
349
+ ? this.options.refetchInterval(this.currentResult.data, this.currentQuery)
350
+ : this.options.refetchInterval ?? false
351
+ }
352
+
353
+ private updateRefetchInterval(nextInterval: number | false): void {
354
+ this.clearRefetchInterval()
355
+
356
+ this.currentRefetchInterval = nextInterval
357
+
358
+ if (
359
+ isServer ||
360
+ this.options.enabled === false ||
361
+ !isValidTimeout(this.currentRefetchInterval) ||
362
+ this.currentRefetchInterval === 0
363
+ ) {
364
+ return
365
+ }
366
+
367
+ this.refetchIntervalId = setInterval(() => {
368
+ if (
369
+ this.options.refetchIntervalInBackground ||
370
+ focusManager.isFocused()
371
+ ) {
372
+ this.executeFetch()
373
+ }
374
+ }, this.currentRefetchInterval)
375
+ }
376
+
377
+ private updateTimers(): void {
378
+ this.updateStaleTimeout()
379
+ this.updateRefetchInterval(this.computeRefetchInterval())
380
+ }
381
+
382
+ private clearStaleTimeout(): void {
383
+ if (this.staleTimeoutId) {
384
+ clearTimeout(this.staleTimeoutId)
385
+ this.staleTimeoutId = undefined
386
+ }
387
+ }
388
+
389
+ private clearRefetchInterval(): void {
390
+ if (this.refetchIntervalId) {
391
+ clearInterval(this.refetchIntervalId)
392
+ this.refetchIntervalId = undefined
393
+ }
394
+ }
395
+
396
+ protected createResult(
397
+ query: Query<TQueryFnData, TError, TQueryData, TQueryKey>,
398
+ options: QueryObserverOptions<
399
+ TQueryFnData,
400
+ TError,
401
+ TData,
402
+ TQueryData,
403
+ TQueryKey
404
+ >,
405
+ ): QueryObserverResult<TData, TError> {
406
+ const prevQuery = this.currentQuery
407
+ const prevOptions = this.options
408
+ const prevResult = this.currentResult as
409
+ | QueryObserverResult<TData, TError>
410
+ | undefined
411
+ const prevResultState = this.currentResultState
412
+ const prevResultOptions = this.currentResultOptions
413
+ const queryChange = query !== prevQuery
414
+ const queryInitialState = queryChange
415
+ ? query.state
416
+ : this.currentQueryInitialState
417
+ const prevQueryResult = queryChange
418
+ ? this.currentResult
419
+ : this.previousQueryResult
420
+
421
+ const { state } = query
422
+ let { dataUpdatedAt, error, errorUpdatedAt, fetchStatus, status } = state
423
+ let isPreviousData = false
424
+ let isPlaceholderData = false
425
+ let data: TData | undefined
426
+
427
+ // Optimistically set result in fetching state if needed
428
+ if (options._optimisticResults) {
429
+ const mounted = this.hasListeners()
430
+
431
+ const fetchOnMount = !mounted && shouldFetchOnMount(query, options)
432
+
433
+ const fetchOptionally =
434
+ mounted && shouldFetchOptionally(query, prevQuery, options, prevOptions)
435
+
436
+ if (fetchOnMount || fetchOptionally) {
437
+ fetchStatus = canFetch(query.options.networkMode)
438
+ ? 'fetching'
439
+ : 'paused'
440
+ if (!dataUpdatedAt) {
441
+ status = 'loading'
442
+ }
443
+ }
444
+ if (options._optimisticResults === 'isRestoring') {
445
+ fetchStatus = 'idle'
446
+ }
447
+ }
448
+
449
+ // Keep previous data if needed
450
+ if (
451
+ options.keepPreviousData &&
452
+ !state.dataUpdateCount &&
453
+ prevQueryResult?.isSuccess &&
454
+ status !== 'error'
455
+ ) {
456
+ data = prevQueryResult.data
457
+ dataUpdatedAt = prevQueryResult.dataUpdatedAt
458
+ status = prevQueryResult.status
459
+ isPreviousData = true
460
+ }
461
+ // Select data if needed
462
+ else if (options.select && typeof state.data !== 'undefined') {
463
+ // Memoize select result
464
+ if (
465
+ prevResult &&
466
+ state.data === prevResultState?.data &&
467
+ options.select === this.selectFn
468
+ ) {
469
+ data = this.selectResult
470
+ } else {
471
+ try {
472
+ this.selectFn = options.select
473
+ data = options.select(state.data)
474
+ data = replaceData(prevResult?.data, data, options)
475
+ this.selectResult = data
476
+ this.selectError = null
477
+ } catch (selectError) {
478
+ if (process.env.NODE_ENV !== 'production') {
479
+ this.client.getLogger().error(selectError)
480
+ }
481
+ this.selectError = selectError as TError
482
+ }
483
+ }
484
+ }
485
+ // Use query data
486
+ else {
487
+ data = state.data as unknown as TData
488
+ }
489
+
490
+ // Show placeholder data if needed
491
+ if (
492
+ typeof options.placeholderData !== 'undefined' &&
493
+ typeof data === 'undefined' &&
494
+ status === 'loading'
495
+ ) {
496
+ let placeholderData
497
+
498
+ // Memoize placeholder data
499
+ if (
500
+ prevResult?.isPlaceholderData &&
501
+ options.placeholderData === prevResultOptions?.placeholderData
502
+ ) {
503
+ placeholderData = prevResult.data
504
+ } else {
505
+ placeholderData =
506
+ typeof options.placeholderData === 'function'
507
+ ? (options.placeholderData as PlaceholderDataFunction<TQueryData>)()
508
+ : options.placeholderData
509
+ if (options.select && typeof placeholderData !== 'undefined') {
510
+ try {
511
+ placeholderData = options.select(placeholderData)
512
+ placeholderData = replaceData(
513
+ prevResult?.data,
514
+ placeholderData,
515
+ options,
516
+ )
517
+ this.selectError = null
518
+ } catch (selectError) {
519
+ if (process.env.NODE_ENV !== 'production') {
520
+ this.client.getLogger().error(selectError)
521
+ }
522
+ this.selectError = selectError as TError
523
+ }
524
+ }
525
+ }
526
+
527
+ if (typeof placeholderData !== 'undefined') {
528
+ status = 'success'
529
+ data = placeholderData as TData
530
+ isPlaceholderData = true
531
+ }
532
+ }
533
+
534
+ if (this.selectError) {
535
+ error = this.selectError as any
536
+ data = this.selectResult
537
+ errorUpdatedAt = Date.now()
538
+ status = 'error'
539
+ }
540
+
541
+ const isFetching = fetchStatus === 'fetching'
542
+
543
+ const result: QueryObserverBaseResult<TData, TError> = {
544
+ status,
545
+ fetchStatus,
546
+ isLoading: status === 'loading',
547
+ isSuccess: status === 'success',
548
+ isError: status === 'error',
549
+ data,
550
+ dataUpdatedAt,
551
+ error,
552
+ errorUpdatedAt,
553
+ failureCount: state.fetchFailureCount,
554
+ errorUpdateCount: state.errorUpdateCount,
555
+ isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
556
+ isFetchedAfterMount:
557
+ state.dataUpdateCount > queryInitialState.dataUpdateCount ||
558
+ state.errorUpdateCount > queryInitialState.errorUpdateCount,
559
+ isFetching: isFetching,
560
+ isRefetching: isFetching && status !== 'loading',
561
+ isLoadingError: status === 'error' && state.dataUpdatedAt === 0,
562
+ isPaused: fetchStatus === 'paused',
563
+ isPlaceholderData,
564
+ isPreviousData,
565
+ isRefetchError: status === 'error' && state.dataUpdatedAt !== 0,
566
+ isStale: isStale(query, options),
567
+ refetch: this.refetch,
568
+ remove: this.remove,
569
+ }
570
+
571
+ return result as QueryObserverResult<TData, TError>
572
+ }
573
+
574
+ updateResult(notifyOptions?: NotifyOptions): void {
575
+ const prevResult = this.currentResult as
576
+ | QueryObserverResult<TData, TError>
577
+ | undefined
578
+
579
+ const nextResult = this.createResult(this.currentQuery, this.options)
580
+ this.currentResultState = this.currentQuery.state
581
+ this.currentResultOptions = this.options
582
+
583
+ // Only notify and update result if something has changed
584
+ if (shallowEqualObjects(nextResult, prevResult)) {
585
+ return
586
+ }
587
+
588
+ this.currentResult = nextResult
589
+
590
+ // Determine which callbacks to trigger
591
+ const defaultNotifyOptions: NotifyOptions = { cache: true }
592
+
593
+ const shouldNotifyListeners = (): boolean => {
594
+ if (!prevResult) {
595
+ return true
596
+ }
597
+
598
+ const { notifyOnChangeProps } = this.options
599
+
600
+ if (
601
+ notifyOnChangeProps === 'all' ||
602
+ (!notifyOnChangeProps && !this.trackedProps.size)
603
+ ) {
604
+ return true
605
+ }
606
+
607
+ const includedProps = new Set(notifyOnChangeProps ?? this.trackedProps)
608
+
609
+ if (this.options.useErrorBoundary) {
610
+ includedProps.add('error')
611
+ }
612
+
613
+ return Object.keys(this.currentResult).some((key) => {
614
+ const typedKey = key as keyof QueryObserverResult
615
+ const changed = this.currentResult[typedKey] !== prevResult[typedKey]
616
+ return changed && includedProps.has(typedKey)
617
+ })
618
+ }
619
+
620
+ if (notifyOptions?.listeners !== false && shouldNotifyListeners()) {
621
+ defaultNotifyOptions.listeners = true
622
+ }
623
+
624
+ this.notify({ ...defaultNotifyOptions, ...notifyOptions })
625
+ }
626
+
627
+ private updateQuery(): void {
628
+ const query = this.client.getQueryCache().build(this.client, this.options)
629
+
630
+ if (query === this.currentQuery) {
631
+ return
632
+ }
633
+
634
+ const prevQuery = this.currentQuery as
635
+ | Query<TQueryFnData, TError, TQueryData, TQueryKey>
636
+ | undefined
637
+ this.currentQuery = query
638
+ this.currentQueryInitialState = query.state
639
+ this.previousQueryResult = this.currentResult
640
+
641
+ if (this.hasListeners()) {
642
+ prevQuery?.removeObserver(this)
643
+ query.addObserver(this)
644
+ }
645
+ }
646
+
647
+ onQueryUpdate(action: Action<TData, TError>): void {
648
+ const notifyOptions: NotifyOptions = {}
649
+
650
+ if (action.type === 'success') {
651
+ notifyOptions.onSuccess = !action.manual
652
+ } else if (action.type === 'error' && !isCancelledError(action.error)) {
653
+ notifyOptions.onError = true
654
+ }
655
+
656
+ this.updateResult(notifyOptions)
657
+
658
+ if (this.hasListeners()) {
659
+ this.updateTimers()
660
+ }
661
+ }
662
+
663
+ private notify(notifyOptions: NotifyOptions): void {
664
+ notifyManager.batch(() => {
665
+ // First trigger the configuration callbacks
666
+ if (notifyOptions.onSuccess) {
667
+ this.options.onSuccess?.(this.currentResult.data!)
668
+ this.options.onSettled?.(this.currentResult.data!, null)
669
+ } else if (notifyOptions.onError) {
670
+ this.options.onError?.(this.currentResult.error!)
671
+ this.options.onSettled?.(undefined, this.currentResult.error!)
672
+ }
673
+
674
+ // Then trigger the listeners
675
+ if (notifyOptions.listeners) {
676
+ this.listeners.forEach((listener) => {
677
+ listener(this.currentResult)
678
+ })
679
+ }
680
+
681
+ // Then the cache listeners
682
+ if (notifyOptions.cache) {
683
+ this.client.getQueryCache().notify({
684
+ query: this.currentQuery,
685
+ type: 'observerResultsUpdated',
686
+ })
687
+ }
688
+ })
689
+ }
690
+ }
691
+
692
+ function shouldLoadOnMount(
693
+ query: Query<any, any, any, any>,
694
+ options: QueryObserverOptions<any, any, any, any>,
695
+ ): boolean {
696
+ return (
697
+ options.enabled !== false &&
698
+ !query.state.dataUpdatedAt &&
699
+ !(query.state.status === 'error' && options.retryOnMount === false)
700
+ )
701
+ }
702
+
703
+ function shouldFetchOnMount(
704
+ query: Query<any, any, any, any>,
705
+ options: QueryObserverOptions<any, any, any, any, any>,
706
+ ): boolean {
707
+ return (
708
+ shouldLoadOnMount(query, options) ||
709
+ (query.state.dataUpdatedAt > 0 &&
710
+ shouldFetchOn(query, options, options.refetchOnMount))
711
+ )
712
+ }
713
+
714
+ function shouldFetchOn(
715
+ query: Query<any, any, any, any>,
716
+ options: QueryObserverOptions<any, any, any, any, any>,
717
+ field: typeof options['refetchOnMount'] &
718
+ typeof options['refetchOnWindowFocus'] &
719
+ typeof options['refetchOnReconnect'],
720
+ ) {
721
+ if (options.enabled !== false) {
722
+ const value = typeof field === 'function' ? field(query) : field
723
+
724
+ return value === 'always' || (value !== false && isStale(query, options))
725
+ }
726
+ return false
727
+ }
728
+
729
+ function shouldFetchOptionally(
730
+ query: Query<any, any, any, any>,
731
+ prevQuery: Query<any, any, any, any>,
732
+ options: QueryObserverOptions<any, any, any, any, any>,
733
+ prevOptions: QueryObserverOptions<any, any, any, any, any>,
734
+ ): boolean {
735
+ return (
736
+ options.enabled !== false &&
737
+ (query !== prevQuery || prevOptions.enabled === false) &&
738
+ (!options.suspense || query.state.status !== 'error') &&
739
+ isStale(query, options)
740
+ )
741
+ }
742
+
743
+ function isStale(
744
+ query: Query<any, any, any, any>,
745
+ options: QueryObserverOptions<any, any, any, any, any>,
746
+ ): boolean {
747
+ return query.isStaleByTime(options.staleTime)
748
+ }