Skip to content

Commit 5e64873

Browse files
authored
fix(query-core): make sure accessing one property on a query result in useQueries tracks properties across all observers (#7014)
* fix(query-core): make sure accessing one property on a query result in useQueries tracks properties across all observers In useQueries, property tracking is still performed on each individual observer. However, not all invocations will access the property on all observers. For example: ``` results.some((result) => result.isLoading) ``` will only track the `isLoading` property of the first query in the array if `isLoading` is true, because the loop will eagerly abort, and thus the property isn't accessed at all on other observer, which can lead to missing re-renders. This fix adds a callback to `trackProperties`, which will be invoked when we start to track a property on an observer. Then, in useQueries, we can use this callback to trigger the accessor manually for all other observers. That makes sure that if a property like `isLoading` is accessed on one observer, it is tracked for all of them. * refactor: expose trackProp function
1 parent b311a0a commit 5e64873

File tree

3 files changed

+63
-2
lines changed

3 files changed

+63
-2
lines changed

packages/query-core/src/queriesObserver.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,12 @@ export class QueriesObserver<
171171
return matches.map((match, index) => {
172172
const observerResult = result[index]!
173173
return !match.defaultedQueryOptions.notifyOnChangeProps
174-
? match.observer.trackResult(observerResult)
174+
? match.observer.trackResult(observerResult, (accessedProp) => {
175+
// track property on all observers to ensure proper (synchronized) tracking (#7000)
176+
matches.forEach((m) => {
177+
m.observer.trackProp(accessedProp)
178+
})
179+
})
175180
: observerResult
176181
})
177182
},

packages/query-core/src/queryObserver.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export class QueryObserver<
247247

248248
trackResult(
249249
result: QueryObserverResult<TData, TError>,
250+
onPropTracked?: (key: keyof QueryObserverResult) => void,
250251
): QueryObserverResult<TData, TError> {
251252
const trackedResult = {} as QueryObserverResult<TData, TError>
252253

@@ -255,7 +256,8 @@ export class QueryObserver<
255256
configurable: false,
256257
enumerable: true,
257258
get: () => {
258-
this.#trackedProps.add(key as keyof QueryObserverResult)
259+
this.trackProp(key as keyof QueryObserverResult)
260+
onPropTracked?.(key as keyof QueryObserverResult)
259261
return result[key as keyof QueryObserverResult]
260262
},
261263
})
@@ -264,6 +266,10 @@ export class QueryObserver<
264266
return trackedResult
265267
}
266268

269+
trackProp(key: keyof QueryObserverResult) {
270+
this.#trackedProps.add(key)
271+
}
272+
267273
getCurrentQuery(): Query<TQueryFnData, TError, TQueryData, TQueryKey> {
268274
return this.#currentQuery
269275
}

packages/react-query/src/__tests__/useQueries.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,56 @@ describe('useQueries', () => {
11851185
expect(results.length).toBe(length)
11861186
})
11871187

1188+
it('should synchronously track properties of all observer even if a property (isLoading) is only accessed on one observer (#7000)', async () => {
1189+
const key = queryKey()
1190+
const ids = [1, 2]
1191+
1192+
function Page() {
1193+
const { isLoading } = useQueries({
1194+
queries: ids.map((id) => ({
1195+
queryKey: [key, id],
1196+
queryFn: () => {
1197+
return new Promise<{
1198+
id: number
1199+
title: string
1200+
}>((resolve, reject) => {
1201+
if (id === 2) {
1202+
setTimeout(() => {
1203+
reject(new Error('FAILURE'))
1204+
}, 10)
1205+
}
1206+
setTimeout(() => {
1207+
resolve({ id, title: `Post ${id}` })
1208+
}, 10)
1209+
})
1210+
},
1211+
retry: false,
1212+
})),
1213+
combine: (results) => {
1214+
// this tracks data on all observers
1215+
void results.forEach((result) => result.data)
1216+
return {
1217+
// .some aborts early, so `isLoading` might not be accessed (and thus tracked) on all observers
1218+
// leading to missing re-renders
1219+
isLoading: results.some((result) => result.isLoading),
1220+
}
1221+
},
1222+
})
1223+
1224+
return (
1225+
<div>
1226+
<p>Loading Status: {isLoading ? 'Loading...' : 'Loaded'}</p>
1227+
</div>
1228+
)
1229+
}
1230+
1231+
const rendered = renderWithClient(queryClient, <Page />)
1232+
1233+
await waitFor(() => rendered.getByText('Loading Status: Loading...'))
1234+
1235+
await waitFor(() => rendered.getByText('Loading Status: Loaded'))
1236+
})
1237+
11881238
it('should not have stale closures with combine (#6648)', async () => {
11891239
const key = queryKey()
11901240

0 commit comments

Comments
 (0)