Skip to content

Commit

Permalink
feat(useQueries): combine (#5219)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
4 people authored May 15, 2023
1 parent e76a2c3 commit 0388a2f
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 74 deletions.
41 changes: 34 additions & 7 deletions docs/react/reference/useQueries.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,50 @@ title: useQueries
The `useQueries` hook can be used to fetch a variable number of queries:

```tsx
const ids = [1,2,3]
const results = useQueries({
queries: [
{ queryKey: ['post', 1], queryFn: fetchPost, staleTime: Infinity},
{ queryKey: ['post', 2], queryFn: fetchPost, staleTime: Infinity}
]
queries: ids.map(id => [
{ queryKey: ['post', id], queryFn: () => fetchPost(id), staleTime: Infinity },
]),
})
```

**Options**

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).
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).

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

> 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.
> 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.
**placeholderData**

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.

**Returns**

The `useQueries` hook returns an array with all the query results. The order returned is the same as the input order.

## Combine

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.

```tsx
const ids = [1,2,3]
const combinedQueries = useQueries({
queries: ids.map(id => [
{ queryKey: ['post', id], queryFn: () => fetchPost(id) },
]),
combine: (results) => {
return ({
data: results.map(result => result.data),
pending: results.some(result => result.isPending),
})
}
})
```

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.
1 change: 1 addition & 0 deletions packages/query-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export type {
DehydratedState,
HydrateOptions,
} from './hydration'
export type { QueriesObserverOptions } from './queriesObserver'
76 changes: 63 additions & 13 deletions packages/query-core/src/queriesObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { QueryClient } from './queryClient'
import type { NotifyOptions } from './queryObserver'
import { QueryObserver } from './queryObserver'
import { Subscribable } from './subscribable'
import { replaceEqualDeep } from './utils'

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

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

export class QueriesObserver extends Subscribable<QueriesObserverListener> {
export interface QueriesObserverOptions<
TCombinedResult = QueryObserverResult[],
> {
combine?: (result: QueryObserverResult[]) => TCombinedResult
}

export class QueriesObserver<
TCombinedResult = QueryObserverResult[],
> extends Subscribable<QueriesObserverListener> {
#client: QueryClient
#result: QueryObserverResult[]
#result!: QueryObserverResult[]
#queries: QueryObserverOptions[]
#observers: QueryObserver[]
#options?: QueriesObserverOptions<TCombinedResult>
#combinedResult!: TCombinedResult

constructor(client: QueryClient, queries?: QueryObserverOptions[]) {
constructor(
client: QueryClient,
queries: QueryObserverOptions[],
options?: QueriesObserverOptions<TCombinedResult>,
) {
super()

this.#client = client
this.#queries = []
this.#result = []
this.#observers = []

if (queries) {
this.setQueries(queries)
}
this.#setResult([])
this.setQueries(queries, options)
}

#setResult(value: QueryObserverResult[]) {
this.#result = value
this.#combinedResult = this.#combineResult(value)
}

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

setQueries(
queries: QueryObserverOptions[],
options?: QueriesObserverOptions<TCombinedResult>,
notifyOptions?: NotifyOptions,
): void {
this.#queries = queries
this.#options = options

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

this.#observers = newObservers
this.#result = newResult
this.#setResult(newResult)

if (!this.hasListeners()) {
return
Expand All @@ -112,8 +132,8 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
})
}

getCurrentResult(): QueryObserverResult[] {
return this.#result
getCurrentResult(): TCombinedResult {
return this.#combinedResult
}

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

getOptimisticResult(queries: QueryObserverOptions[]): QueryObserverResult[] {
return this.#findMatchingObservers(queries).map((match) =>
getOptimisticResult(
queries: QueryObserverOptions[],
): [
rawResult: QueryObserverResult[],
combineResult: (r?: QueryObserverResult[]) => TCombinedResult,
trackResult: () => QueryObserverResult[],
] {
const matches = this.#findMatchingObservers(queries)
const result = matches.map((match) =>
match.observer.getOptimisticResult(match.defaultedQueryOptions),
)

return [
result,
(r?: QueryObserverResult[]) => {
return this.#combineResult(r ?? result)
},
() => {
return matches.map((match, index) => {
const observerResult = result[index]!
return !match.defaultedQueryOptions.notifyOnChangeProps
? match.observer.trackResult(observerResult)
: observerResult
})
},
]
}

#combineResult(input: QueryObserverResult[]): TCombinedResult {
const combine = this.#options?.combine
if (combine) {
return replaceEqualDeep(this.#combinedResult, combine(input))
}
return input as any
}

#findMatchingObservers(
Expand Down Expand Up @@ -192,7 +242,7 @@ export class QueriesObserver extends Subscribable<QueriesObserverListener> {
#onUpdate(observer: QueryObserver, result: QueryObserverResult): void {
const index = this.#observers.indexOf(observer)
if (index !== -1) {
this.#result = replaceAt(this.#result, index, result)
this.#setResult(replaceAt(this.#result, index, result))
this.#notify()
}
}
Expand Down
Loading

0 comments on commit 0388a2f

Please sign in to comment.