Skip to content

Commit 0388a2f

Browse files
TkDodoDamianOsipiuklachlancollinsardeora
authored
feat(useQueries): combine (#5219)
* attempt at adding combine on observer level (doesn't work) * feat(useQueries): combine adapt getOptimisticResult to return both the result array and a combined result getter * feat(useQueries): combine make sure combinedResult stays in sync with result * feat(vue-query): combine results for useQueries hook * Add new options to svelte-query * Add new options to solid-query * fix: enable property tracking for useQueries * fix: move property tracking to react layer * chore: remove logging * chore: remove unnecessary type assertion * test: tests for combined data * docs: combine --------- Co-authored-by: Damian Osipiuk <[email protected]> Co-authored-by: Lachlan Collins <[email protected]> Co-authored-by: Aryan Deora <[email protected]>
1 parent e76a2c3 commit 0388a2f

File tree

10 files changed

+453
-74
lines changed

10 files changed

+453
-74
lines changed

docs/react/reference/useQueries.md

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,50 @@ title: useQueries
66
The `useQueries` hook can be used to fetch a variable number of queries:
77

88
```tsx
9+
const ids = [1,2,3]
910
const results = useQueries({
10-
queries: [
11-
{ queryKey: ['post', 1], queryFn: fetchPost, staleTime: Infinity},
12-
{ queryKey: ['post', 2], queryFn: fetchPost, staleTime: Infinity}
13-
]
11+
queries: ids.map(id => [
12+
{ queryKey: ['post', id], queryFn: () => fetchPost(id), staleTime: Infinity },
13+
]),
1414
})
1515
```
1616

1717
**Options**
1818

19-
The `useQueries` hook accepts an options object with a **queries** key whose value is an array with query option objects identical to the [`useQuery` hook](../reference/useQuery) (excluding the `context` option).
19+
The `useQueries` hook accepts an options object with a **queries** key whose value is an array with query option objects identical to the [`useQuery` hook](../reference/useQuery) (excluding the `queryClient` option - because the `QueryClient` can be passed in on the top level).
2020

2121
- `queryClient?: QueryClient`,
22-
- Use this to use a custom QueryClient. Otherwise, the one from the nearest context will be used.
22+
- Use this to provide a custom QueryClient. Otherwise, the one from the nearest context will be used.
23+
- `combine?`: (result: UseQueriesResults) => TCombinedResult
24+
- Use this to combine the results of the queries into a single value.
2325

24-
> Having the same query key more than once in the array of query objects may cause some data to be shared between queries, e.g. when using `placeholderData` and `select`. To avoid this, consider de-duplicating the queries and map the results back to the desired structure.
26+
> Having the same query key more than once in the array of query objects may cause some data to be shared between queries. To avoid this, consider de-duplicating the queries and map the results back to the desired structure.
27+
28+
**placeholderData**
29+
30+
The `placeholderData` option exists for `useQueries` as well, but it doesn't get information passed from previously rendered Queries like `useQuery` does, because the input to `useQueries` can be a different number of Queries on each render.
2531

2632
**Returns**
2733

2834
The `useQueries` hook returns an array with all the query results. The order returned is the same as the input order.
35+
36+
## Combine
37+
38+
If you want to combine `data` (or other Query information) from the results into a single value, you can use the `combine` option. The result will be structurally shared to be as referentially stable as possible.
39+
40+
```tsx
41+
const ids = [1,2,3]
42+
const combinedQueries = useQueries({
43+
queries: ids.map(id => [
44+
{ queryKey: ['post', id], queryFn: () => fetchPost(id) },
45+
]),
46+
combine: (results) => {
47+
return ({
48+
data: results.map(result => result.data),
49+
pending: results.some(result => result.isPending),
50+
})
51+
}
52+
})
53+
```
54+
55+
In the above example, `combinedQueries` will be an object with a `data` and a `pending` property. Note that all other properties of the Query results will be lost.

packages/query-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ export type {
3636
DehydratedState,
3737
HydrateOptions,
3838
} from './hydration'
39+
export type { QueriesObserverOptions } from './queriesObserver'

packages/query-core/src/queriesObserver.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { QueryClient } from './queryClient'
88
import type { NotifyOptions } from './queryObserver'
99
import { QueryObserver } from './queryObserver'
1010
import { Subscribable } from './subscribable'
11+
import { replaceEqualDeep } from './utils'
1112

1213
function difference<T>(array1: T[], array2: T[]): T[] {
1314
return array1.filter((x) => array2.indexOf(x) === -1)
@@ -21,23 +22,40 @@ function replaceAt<T>(array: T[], index: number, value: T): T[] {
2122

2223
type QueriesObserverListener = (result: QueryObserverResult[]) => void
2324

24-
export class QueriesObserver extends Subscribable<QueriesObserverListener> {
25+
export interface QueriesObserverOptions<
26+
TCombinedResult = QueryObserverResult[],
27+
> {
28+
combine?: (result: QueryObserverResult[]) => TCombinedResult
29+
}
30+
31+
export class QueriesObserver<
32+
TCombinedResult = QueryObserverResult[],
33+
> extends Subscribable<QueriesObserverListener> {
2534
#client: QueryClient
26-
#result: QueryObserverResult[]
35+
#result!: QueryObserverResult[]
2736
#queries: QueryObserverOptions[]
2837
#observers: QueryObserver[]
38+
#options?: QueriesObserverOptions<TCombinedResult>
39+
#combinedResult!: TCombinedResult
2940

30-
constructor(client: QueryClient, queries?: QueryObserverOptions[]) {
41+
constructor(
42+
client: QueryClient,
43+
queries: QueryObserverOptions[],
44+
options?: QueriesObserverOptions<TCombinedResult>,
45+
) {
3146
super()
3247

3348
this.#client = client
3449
this.#queries = []
35-
this.#result = []
3650
this.#observers = []
3751

38-
if (queries) {
39-
this.setQueries(queries)
40-
}
52+
this.#setResult([])
53+
this.setQueries(queries, options)
54+
}
55+
56+
#setResult(value: QueryObserverResult[]) {
57+
this.#result = value
58+
this.#combinedResult = this.#combineResult(value)
4159
}
4260

4361
protected onSubscribe(): void {
@@ -65,9 +83,11 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
6583

6684
setQueries(
6785
queries: QueryObserverOptions[],
86+
options?: QueriesObserverOptions<TCombinedResult>,
6887
notifyOptions?: NotifyOptions,
6988
): void {
7089
this.#queries = queries
90+
this.#options = options
7191

7292
notifyManager.batch(() => {
7393
const prevObservers = this.#observers
@@ -92,7 +112,7 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
92112
}
93113

94114
this.#observers = newObservers
95-
this.#result = newResult
115+
this.#setResult(newResult)
96116

97117
if (!this.hasListeners()) {
98118
return
@@ -112,8 +132,8 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
112132
})
113133
}
114134

115-
getCurrentResult(): QueryObserverResult[] {
116-
return this.#result
135+
getCurrentResult(): TCombinedResult {
136+
return this.#combinedResult
117137
}
118138

119139
getQueries() {
@@ -124,10 +144,40 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
124144
return this.#observers
125145
}
126146

127-
getOptimisticResult(queries: QueryObserverOptions[]): QueryObserverResult[] {
128-
return this.#findMatchingObservers(queries).map((match) =>
147+
getOptimisticResult(
148+
queries: QueryObserverOptions[],
149+
): [
150+
rawResult: QueryObserverResult[],
151+
combineResult: (r?: QueryObserverResult[]) => TCombinedResult,
152+
trackResult: () => QueryObserverResult[],
153+
] {
154+
const matches = this.#findMatchingObservers(queries)
155+
const result = matches.map((match) =>
129156
match.observer.getOptimisticResult(match.defaultedQueryOptions),
130157
)
158+
159+
return [
160+
result,
161+
(r?: QueryObserverResult[]) => {
162+
return this.#combineResult(r ?? result)
163+
},
164+
() => {
165+
return matches.map((match, index) => {
166+
const observerResult = result[index]!
167+
return !match.defaultedQueryOptions.notifyOnChangeProps
168+
? match.observer.trackResult(observerResult)
169+
: observerResult
170+
})
171+
},
172+
]
173+
}
174+
175+
#combineResult(input: QueryObserverResult[]): TCombinedResult {
176+
const combine = this.#options?.combine
177+
if (combine) {
178+
return replaceEqualDeep(this.#combinedResult, combine(input))
179+
}
180+
return input as any
131181
}
132182

133183
#findMatchingObservers(
@@ -192,7 +242,7 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
192242
#onUpdate(observer: QueryObserver, result: QueryObserverResult): void {
193243
const index = this.#observers.indexOf(observer)
194244
if (index !== -1) {
195-
this.#result = replaceAt(this.#result, index, result)
245+
this.#setResult(replaceAt(this.#result, index, result))
196246
this.#notify()
197247
}
198248
}

0 commit comments

Comments
 (0)