Skip to content

Commit 397d8b7

Browse files
authored
fix: notify on updates between mounting and subscribing (#1737)
1 parent 1032804 commit 397d8b7

File tree

5 files changed

+181
-98
lines changed

5 files changed

+181
-98
lines changed

src/core/infiniteQueryObserver.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,9 @@ export class InfiniteQueryObserver<
9898
})
9999
}
100100

101-
protected getNewResult(
102-
willFetch?: boolean
103-
): InfiniteQueryObserverResult<TData, TError> {
101+
protected getNewResult(): InfiniteQueryObserverResult<TData, TError> {
104102
const { state } = this.getCurrentQuery()
105-
const result = super.getNewResult(willFetch)
103+
const result = super.getNewResult()
106104
return {
107105
...result,
108106
fetchNextPage: this.fetchNextPage,

src/core/queryObserver.ts

Lines changed: 93 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class QueryObserver<
8787
this.executeFetch()
8888
}
8989

90+
this.updateResult()
9091
this.updateTimers()
9192
}
9293
}
@@ -155,7 +156,6 @@ export class QueryObserver<
155156
options?: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
156157
): void {
157158
const prevOptions = this.options
158-
const prevQuery = this.currentQuery
159159

160160
this.options = this.client.defaultQueryObserverOptions(options)
161161

@@ -171,39 +171,60 @@ export class QueryObserver<
171171
this.options.queryKey = prevOptions.queryKey
172172
}
173173

174-
this.updateQuery()
174+
const didUpdateQuery = this.updateQuery()
175175

176-
// Take no further actions if there are no subscribers
177-
if (!this.listeners.length) {
178-
return
179-
}
176+
let optionalFetch
177+
let updateStaleTimeout
178+
let updateRefetchInterval
180179

181-
// If we subscribed to a new query, optionally fetch and update refetch
182-
if (this.currentQuery !== prevQuery) {
183-
this.optionalFetch()
184-
this.updateTimers()
185-
return
180+
// If we subscribed to a new query, optionally fetch and update intervals
181+
if (didUpdateQuery) {
182+
optionalFetch = true
183+
updateStaleTimeout = true
184+
updateRefetchInterval = true
186185
}
187186

188187
// Optionally fetch if the query became enabled
189188
if (this.options.enabled !== false && prevOptions.enabled === false) {
190-
this.optionalFetch()
189+
optionalFetch = true
191190
}
192191

193192
// Update stale interval if needed
194193
if (
195194
this.options.enabled !== prevOptions.enabled ||
196195
this.options.staleTime !== prevOptions.staleTime
197196
) {
198-
this.updateStaleTimeout()
197+
updateStaleTimeout = true
199198
}
200199

201200
// Update refetch interval if needed
202201
if (
203202
this.options.enabled !== prevOptions.enabled ||
204203
this.options.refetchInterval !== prevOptions.refetchInterval
205204
) {
206-
this.updateRefetchInterval()
205+
updateRefetchInterval = true
206+
}
207+
208+
// Fetch only if there are subscribers
209+
if (this.hasListeners()) {
210+
if (optionalFetch) {
211+
this.optionalFetch()
212+
}
213+
}
214+
215+
// Update result when subscribing to a new query
216+
if (didUpdateQuery) {
217+
this.updateResult()
218+
}
219+
220+
// Update intervals only if there are subscribers
221+
if (this.hasListeners()) {
222+
if (updateStaleTimeout) {
223+
this.updateStaleTimeout()
224+
}
225+
if (updateRefetchInterval) {
226+
this.updateRefetchInterval()
227+
}
207228
}
208229
}
209230

@@ -302,12 +323,7 @@ export class QueryObserver<
302323

303324
this.staleTimeoutId = setTimeout(() => {
304325
if (!this.currentResult.isStale) {
305-
const prevResult = this.currentResult
306326
this.updateResult()
307-
this.notify({
308-
listeners: this.shouldNotifyListeners(prevResult, this.currentResult),
309-
cache: true,
310-
})
311327
}
312328
}, timeout)
313329
}
@@ -353,9 +369,7 @@ export class QueryObserver<
353369
this.refetchIntervalId = undefined
354370
}
355371

356-
protected getNewResult(
357-
willFetch?: boolean
358-
): QueryObserverResult<TData, TError> {
372+
protected getNewResult(): QueryObserverResult<TData, TError> {
359373
const { state } = this.currentQuery
360374
let { isFetching, status } = state
361375
let isPreviousData = false
@@ -364,7 +378,7 @@ export class QueryObserver<
364378
let dataUpdatedAt = state.dataUpdatedAt
365379

366380
// Optimistically set status to loading if we will start fetching
367-
if (willFetch) {
381+
if (!this.hasListeners() && this.willFetchOnMount()) {
368382
isFetching = true
369383
if (!dataUpdatedAt) {
370384
status = 'loading'
@@ -442,7 +456,7 @@ export class QueryObserver<
442456
}
443457

444458
private shouldNotifyListeners(
445-
prevResult: QueryObserverResult,
459+
prevResult: QueryObserverResult | undefined,
446460
result: QueryObserverResult
447461
): boolean {
448462
const { notifyOnChangeProps, notifyOnChangePropsExclusions } = this.options
@@ -451,6 +465,10 @@ export class QueryObserver<
451465
return false
452466
}
453467

468+
if (!prevResult) {
469+
return true
470+
}
471+
454472
if (!notifyOnChangeProps && !notifyOnChangePropsExclusions) {
455473
return true
456474
}
@@ -485,39 +503,60 @@ export class QueryObserver<
485503
return false
486504
}
487505

488-
private updateResult(willFetch?: boolean): void {
489-
const result = this.getNewResult(willFetch)
506+
private updateResult(action?: Action<TData, TError>): void {
507+
const prevResult = this.currentResult as
508+
| QueryObserverResult<TData, TError>
509+
| undefined
510+
511+
const result = this.getNewResult()
490512

491513
// Keep reference to the current state on which the current result is based on
492514
this.currentResultState = this.currentQuery.state
493515

494516
// Only update if something has changed
495-
if (!shallowEqualObjects(result, this.currentResult)) {
496-
this.currentResult = result
517+
if (shallowEqualObjects(result, prevResult)) {
518+
return
519+
}
497520

498-
if (this.options.notifyOnChangeProps === 'tracked') {
499-
const addTrackedProps = (prop: keyof QueryObserverResult) => {
500-
if (!this.trackedProps.includes(prop)) {
501-
this.trackedProps.push(prop)
502-
}
521+
this.currentResult = result
522+
523+
if (this.options.notifyOnChangeProps === 'tracked') {
524+
const addTrackedProps = (prop: keyof QueryObserverResult) => {
525+
if (!this.trackedProps.includes(prop)) {
526+
this.trackedProps.push(prop)
503527
}
504-
this.trackedCurrentResult = {} as QueryObserverResult<TData, TError>
505-
506-
Object.keys(result).forEach(key => {
507-
Object.defineProperty(this.trackedCurrentResult, key, {
508-
configurable: false,
509-
enumerable: true,
510-
get() {
511-
addTrackedProps(key as keyof QueryObserverResult)
512-
return result[key as keyof QueryObserverResult]
513-
},
514-
})
515-
})
516528
}
529+
this.trackedCurrentResult = {} as QueryObserverResult<TData, TError>
530+
531+
Object.keys(result).forEach(key => {
532+
Object.defineProperty(this.trackedCurrentResult, key, {
533+
configurable: false,
534+
enumerable: true,
535+
get() {
536+
addTrackedProps(key as keyof QueryObserverResult)
537+
return result[key as keyof QueryObserverResult]
538+
},
539+
})
540+
})
541+
}
542+
543+
// Determine which callbacks to trigger
544+
const notifyOptions: NotifyOptions = { cache: true }
545+
546+
if (action?.type === 'success') {
547+
notifyOptions.onSuccess = true
548+
} else if (action?.type === 'error') {
549+
notifyOptions.onError = true
517550
}
551+
552+
if (this.shouldNotifyListeners(prevResult, result)) {
553+
notifyOptions.listeners = true
554+
}
555+
556+
this.notify(notifyOptions)
518557
}
519558

520-
private updateQuery(): void {
559+
private updateQuery(): boolean {
521560
const prevQuery = this.currentQuery
522561

523562
const query = this.client
@@ -528,62 +567,27 @@ export class QueryObserver<
528567
)
529568

530569
if (query === prevQuery) {
531-
return
570+
return false
532571
}
533572

534573
this.previousQueryResult = this.currentResult
535574
this.currentQuery = query
536575
this.initialDataUpdateCount = query.state.dataUpdateCount
537576
this.initialErrorUpdateCount = query.state.errorUpdateCount
538577

539-
const willFetch = prevQuery
540-
? this.willFetchOptionally()
541-
: this.willFetchOnMount()
542-
543-
this.updateResult(willFetch)
544-
545-
if (!this.hasListeners()) {
546-
return
578+
if (this.hasListeners()) {
579+
prevQuery?.removeObserver(this)
580+
this.currentQuery.addObserver(this)
547581
}
548582

549-
prevQuery?.removeObserver(this)
550-
this.currentQuery.addObserver(this)
551-
552-
if (
553-
this.shouldNotifyListeners(this.previousQueryResult, this.currentResult)
554-
) {
555-
this.notify({ listeners: true })
556-
}
583+
return true
557584
}
558585

559586
onQueryUpdate(action: Action<TData, TError>): void {
560-
// Store current result and get new result
561-
const prevResult = this.currentResult
562-
this.updateResult()
563-
const currentResult = this.currentResult
564-
565-
// Update timers
566-
this.updateTimers()
567-
568-
// Do not notify if the nothing has changed
569-
if (prevResult === currentResult) {
570-
return
571-
}
572-
573-
// Determine which callbacks to trigger
574-
const notifyOptions: NotifyOptions = {}
575-
576-
if (action.type === 'success') {
577-
notifyOptions.onSuccess = true
578-
} else if (action.type === 'error') {
579-
notifyOptions.onError = true
580-
}
581-
582-
if (this.shouldNotifyListeners(prevResult, currentResult)) {
583-
notifyOptions.listeners = true
587+
this.updateResult(action)
588+
if (this.hasListeners()) {
589+
this.updateTimers()
584590
}
585-
586-
this.notify(notifyOptions)
587591
}
588592

589593
private notify(notifyOptions: NotifyOptions): void {

src/core/tests/queryClient.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ describe('queryClient', () => {
7777
const observer = new QueryObserver(queryClient, {
7878
queryKey: [key],
7979
retry: false,
80+
enabled: false,
8081
})
8182
const { status } = await observer.refetch()
8283
expect(status).toBe('error')

src/core/tests/queryObserver.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ describe('queryObserver', () => {
4949
expect(results[2]).toMatchObject({ data: 2, status: 'success' })
5050
})
5151

52+
test('should notify when the query has updated before subscribing', async () => {
53+
const key = queryKey()
54+
const results: QueryObserverResult[] = []
55+
const observer = new QueryObserver(queryClient, {
56+
queryKey: key,
57+
queryFn: () => 1,
58+
staleTime: Infinity,
59+
})
60+
queryClient.setQueryData(key, 2)
61+
const unsubscribe = observer.subscribe(result => {
62+
results.push(result)
63+
})
64+
await sleep(1)
65+
unsubscribe()
66+
expect(results.length).toBe(1)
67+
expect(results[0]).toMatchObject({ data: 2, status: 'success' })
68+
})
69+
5270
test('should be able to fetch with a selector', async () => {
5371
const key = queryKey()
5472
const observer = new QueryObserver(queryClient, {

0 commit comments

Comments
 (0)