Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useQueries): combine #5219

Merged
merged 23 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1fd39ec
attempt at adding combine on observer level (doesn't work)
TkDodo Apr 1, 2023
a47b562
Merge branch 'alpha' into feature/usequeries-combine
TkDodo Apr 2, 2023
c454adf
Merge branch 'alpha' into feature/usequeries-combine
TkDodo Apr 2, 2023
e946171
feat(useQueries): combine
TkDodo Apr 2, 2023
5394172
feat(useQueries): combine
TkDodo Apr 2, 2023
00bcd2a
feat(vue-query): combine results for useQueries hook
DamianOsipiuk Apr 11, 2023
28a038d
Merge branch 'alpha' into feature/usequeries-combine
TkDodo Apr 15, 2023
9c17759
Merge branch 'alpha' into feature/usequeries-combine
TkDodo Apr 30, 2023
2e1f649
Merge branch 'alpha' into feature/usequeries-combine
TkDodo Apr 30, 2023
0a40ca1
Merge branch 'alpha' into feature/usequeries-combine
TkDodo May 1, 2023
b2d3666
Add new options to svelte-query
lachlancollins May 1, 2023
51b8d42
Add new options to solid-query
ardeora May 2, 2023
afbe137
Merge branch 'feature/usequeries-combine' of https://github.com/TanSt…
ardeora May 2, 2023
28d4ea1
Merge branch 'alpha' into feature/usequeries-combine
TkDodo May 13, 2023
8b6533c
fix: enable property tracking for useQueries
TkDodo May 13, 2023
36561f8
Merge branch 'alpha' into feature/usequeries-combine
TkDodo May 13, 2023
0a95924
fix: move property tracking to react layer
TkDodo May 13, 2023
ef14f03
chore: remove logging
TkDodo May 13, 2023
c65287a
chore: remove unnecessary type assertion
TkDodo May 13, 2023
3ba6746
Merge branch 'alpha' into feature/usequeries-combine
TkDodo May 14, 2023
ef49355
test: tests for combined data
TkDodo May 15, 2023
35149d0
docs: combine
TkDodo May 15, 2023
b318428
Merge branch 'alpha' into feature/usequeries-combine
TkDodo May 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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