Skip to content

Commit c51498a

Browse files
authored
fix(useQueries): search for unused observers if keepPreviousData (TanStack#2680) (TanStack#2866)
1 parent c6906b3 commit c51498a

File tree

3 files changed

+320
-42
lines changed

3 files changed

+320
-42
lines changed

src/core/queriesObserver.ts

Lines changed: 80 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -64,57 +64,94 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
6464
}
6565

6666
getOptimisticResult(queries: QueryObserverOptions[]): QueryObserverResult[] {
67-
return queries.map((options, index) => {
68-
const defaultedOptions = this.client.defaultQueryObserverOptions(options)
69-
return this.getObserver(defaultedOptions, index).getOptimisticResult(
70-
defaultedOptions
71-
)
72-
})
67+
return this.findMatchingObservers(queries).map(match =>
68+
match.observer.getCurrentResult()
69+
)
7370
}
7471

75-
private getObserver(
76-
options: QueryObserverOptions,
77-
index: number
78-
): QueryObserver {
72+
private findMatchingObservers(
73+
queries: QueryObserverOptions[]
74+
): QueryObserverMatch[] {
75+
const prevObservers = this.observers
76+
const defaultedQueryOptions = queries.map(options =>
77+
this.client.defaultQueryObserverOptions(options)
78+
)
79+
80+
const matchingObservers: QueryObserverMatch[] = defaultedQueryOptions.flatMap(
81+
defaultedOptions => {
82+
const match = prevObservers.find(
83+
observer => observer.options.queryHash === defaultedOptions.queryHash
84+
)
85+
if (match != null) {
86+
return [{ defaultedQueryOptions: defaultedOptions, observer: match }]
87+
}
88+
return []
89+
}
90+
)
91+
92+
const matchedQueryHashes = matchingObservers.map(
93+
match => match.defaultedQueryOptions.queryHash
94+
)
95+
const unmatchedQueries = defaultedQueryOptions.filter(
96+
defaultedOptions =>
97+
!matchedQueryHashes.includes(defaultedOptions.queryHash)
98+
)
99+
100+
const unmatchedObservers = prevObservers.filter(
101+
prevObserver =>
102+
!matchingObservers.some(match => match.observer === prevObserver)
103+
)
104+
105+
const newOrReusedObservers: QueryObserverMatch[] = unmatchedQueries.map(
106+
(options, index) => {
107+
if (options.keepPreviousData) {
108+
// return previous data from one of the observers that no longer match
109+
const previouslyUsedObserver = unmatchedObservers[index]
110+
if (previouslyUsedObserver !== undefined) {
111+
return {
112+
defaultedQueryOptions: options,
113+
observer: previouslyUsedObserver,
114+
}
115+
}
116+
}
117+
return {
118+
defaultedQueryOptions: options,
119+
observer: this.getObserver(options),
120+
}
121+
}
122+
)
123+
124+
return matchingObservers.concat(newOrReusedObservers)
125+
}
126+
127+
private getObserver(options: QueryObserverOptions): QueryObserver {
79128
const defaultedOptions = this.client.defaultQueryObserverOptions(options)
80-
let currentObserver = this.observersMap[defaultedOptions.queryHash!]
81-
if (!currentObserver && defaultedOptions.keepPreviousData) {
82-
currentObserver = this.observers[index]
83-
}
129+
const currentObserver = this.observersMap[defaultedOptions.queryHash!]
84130
return currentObserver ?? new QueryObserver(this.client, defaultedOptions)
85131
}
86132

87133
private updateObservers(notifyOptions?: NotifyOptions): void {
88134
notifyManager.batch(() => {
89-
let hasIndexChange = false
90-
91135
const prevObservers = this.observers
92-
const prevObserversMap = this.observersMap
93136

94-
const newResult: QueryObserverResult[] = []
95-
const newObservers: QueryObserver[] = []
96-
const newObserversMap: Record<string, QueryObserver> = {}
137+
const newObserverMatches = this.findMatchingObservers(this.queries)
97138

98-
this.queries.forEach((options, i) => {
99-
const defaultedOptions = this.client.defaultQueryObserverOptions(
100-
options
101-
)
102-
const queryHash = defaultedOptions.queryHash!
103-
const observer = this.getObserver(defaultedOptions, i)
104-
105-
if (prevObserversMap[queryHash] || defaultedOptions.keepPreviousData) {
106-
observer.setOptions(defaultedOptions, notifyOptions)
107-
}
108-
109-
if (observer !== prevObservers[i]) {
110-
hasIndexChange = true
111-
}
139+
// set options for the new observers to notify of changes
140+
newObserverMatches.forEach(match =>
141+
match.observer.setOptions(match.defaultedQueryOptions, notifyOptions)
142+
)
112143

113-
newObservers.push(observer)
114-
newResult.push(observer.getCurrentResult())
115-
newObserversMap[queryHash] = observer
116-
})
144+
const newObservers = newObserverMatches.map(match => match.observer)
145+
const newObserversMap = Object.fromEntries(
146+
newObservers.map(observer => [observer.options.queryHash, observer])
147+
)
148+
const newResult = newObservers.map(observer =>
149+
observer.getCurrentResult()
150+
)
117151

152+
const hasIndexChange = newObservers.some(
153+
(observer, index) => observer !== prevObservers[index]
154+
)
118155
if (prevObservers.length === newObservers.length && !hasIndexChange) {
119156
return
120157
}
@@ -157,3 +194,8 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
157194
})
158195
}
159196
}
197+
198+
type QueryObserverMatch = {
199+
defaultedQueryOptions: QueryObserverOptions
200+
observer: QueryObserver
201+
}

src/core/tests/queriesObserver.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,51 @@ describe('queriesObserver', () => {
3838
expect(observerResult).toMatchObject([{ data: 1 }, { data: 2 }])
3939
})
4040

41+
test('should still return value for undefined query key', async () => {
42+
const key1 = queryKey()
43+
const queryFn1 = jest.fn().mockReturnValue(1)
44+
const queryFn2 = jest.fn().mockReturnValue(2)
45+
const observer = new QueriesObserver(queryClient, [
46+
{ queryKey: key1, queryFn: queryFn1 },
47+
{ queryKey: undefined, queryFn: queryFn2 },
48+
])
49+
let observerResult
50+
const unsubscribe = observer.subscribe(result => {
51+
observerResult = result
52+
})
53+
await sleep(1)
54+
unsubscribe()
55+
expect(observerResult).toMatchObject([{ data: 1 }, { data: 2 }])
56+
})
57+
58+
test('should return same value for multiple falsy query keys', async () => {
59+
const queryFn1 = jest.fn().mockReturnValue(1)
60+
const queryFn2 = jest.fn().mockReturnValue(2)
61+
const observer = new QueriesObserver(queryClient, [
62+
{ queryKey: undefined, queryFn: queryFn1 },
63+
])
64+
const results: QueryObserverResult[][] = []
65+
results.push(observer.getCurrentResult())
66+
const unsubscribe = observer.subscribe(result => {
67+
results.push(result)
68+
})
69+
await sleep(1)
70+
observer.setQueries([
71+
{ queryKey: undefined, queryFn: queryFn1 },
72+
{ queryKey: '', queryFn: queryFn2 },
73+
])
74+
await sleep(1)
75+
unsubscribe()
76+
expect(results.length).toBe(4)
77+
expect(results[0]).toMatchObject([{ status: 'idle', data: undefined }])
78+
expect(results[1]).toMatchObject([{ status: 'loading', data: undefined }])
79+
expect(results[2]).toMatchObject([{ status: 'success', data: 1 }])
80+
expect(results[3]).toMatchObject([
81+
{ status: 'success', data: 1 },
82+
{ status: 'success', data: 1 },
83+
])
84+
})
85+
4186
test('should update when a query updates', async () => {
4287
const key1 = queryKey()
4388
const key2 = queryKey()

0 commit comments

Comments
 (0)