Skip to content

Commit 21e9f59

Browse files
committed
feat(query-core): add structuralSharing option to useQueries
Add a `structuralSharing` option to useQueries/createQueries/injectQueries that allows disabling structural sharing for the combined result. When set to `false`, the combined result will not use `replaceEqualDeep` for referential stability. Defaults to `true`.
1 parent d688458 commit 21e9f59

9 files changed

Lines changed: 97 additions & 13 deletions

File tree

.changeset/silver-coins-mix.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@tanstack/angular-query-experimental': minor
3+
'@tanstack/svelte-query': minor
4+
'@tanstack/react-query': minor
5+
'@tanstack/solid-query': minor
6+
'@tanstack/query-core': minor
7+
'@tanstack/vue-query': minor
8+
---
9+
10+
feat(query-core): Allow disabling structuralSharing for useQueries.

docs/framework/react/reference/useQueries.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ The `useQueries` hook accepts an options object with a **queries** key whose val
2424
- Use this to provide a custom QueryClient. Otherwise, the one from the nearest context will be used.
2525
- `combine?: (result: UseQueriesResults) => TCombinedResult`
2626
- Use this to combine the results of the queries into a single value.
27+
- `structuralSharing?: boolean`
28+
- Set this to `false` to disable structural sharing between query results when `combine` is provided.
29+
- Defaults to `true`.
2730

2831
> 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.
2932
@@ -37,7 +40,7 @@ The `useQueries` hook returns an array with all the query results. The order ret
3740

3841
## Combine
3942

40-
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.
43+
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. If you want to disable structural sharing for the combined result, you can set the `structuralSharing` option to `false`.
4144

4245
```tsx
4346
const ids = [1, 2, 3]

packages/angular-query-experimental/src/inject-queries.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ export interface InjectQueriesOptions<
211211
...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries<T[K]> },
212212
]
213213
combine?: (result: QueriesResults<T>) => TCombinedResult
214+
/**
215+
* Set this to `false` to disable structural sharing between query results.
216+
* Only applies when `combine` is provided.
217+
* Defaults to `true`.
218+
*/
219+
structuralSharing?: boolean
214220
}
215221

216222
/**
@@ -271,6 +277,8 @@ export function injectQueries<
271277
observerSignal().getOptimisticResult(
272278
defaultedQueries(),
273279
(optionsSignal() as QueriesObserverOptions<TCombinedResult>).combine,
280+
(optionsSignal() as QueriesObserverOptions<TCombinedResult>)
281+
.structuralSharing,
274282
),
275283
)
276284

packages/query-core/src/__tests__/queriesObserver.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ describe('queriesObserver', () => {
300300
{ queryKey: key1, queryFn: queryFn1 },
301301
],
302302
undefined,
303+
undefined,
303304
)[0],
304305
)
305306

@@ -414,6 +415,7 @@ describe('queriesObserver', () => {
414415
const [initialRaw, getInitialCombined] = observer.getOptimisticResult(
415416
[{ queryKey: key1, queryFn: queryFn1 }],
416417
combine,
418+
undefined,
417419
)
418420
const initialCombined = getInitialCombined(initialRaw)
419421

@@ -426,6 +428,7 @@ describe('queriesObserver', () => {
426428
const [newRaw, getNewCombined] = observer.getOptimisticResult(
427429
newQueries,
428430
combine,
431+
undefined,
429432
)
430433
const newCombined = getNewCombined(newRaw)
431434

@@ -461,6 +464,7 @@ describe('queriesObserver', () => {
461464
{ queryKey: key2, queryFn: queryFn2 },
462465
],
463466
combine,
467+
undefined,
464468
)
465469
const initialCombined = getInitialCombined(initialRaw)
466470

@@ -470,6 +474,7 @@ describe('queriesObserver', () => {
470474
const [newRaw, getNewCombined] = observer.getOptimisticResult(
471475
newQueries,
472476
combine,
477+
undefined,
473478
)
474479
const newCombined = getNewCombined(newRaw)
475480

@@ -497,6 +502,7 @@ describe('queriesObserver', () => {
497502
const [initialRaw, getInitialCombined] = observer.getOptimisticResult(
498503
[{ queryKey: key1, queryFn: queryFn1 }],
499504
combine,
505+
undefined,
500506
)
501507
const initialCombined = getInitialCombined(initialRaw)
502508

@@ -505,6 +511,7 @@ describe('queriesObserver', () => {
505511
const [newRaw, getNewCombined] = observer.getOptimisticResult(
506512
[{ queryKey: key2, queryFn: queryFn2 }],
507513
combine,
514+
undefined,
508515
)
509516
const newCombined = getNewCombined(newRaw)
510517

packages/query-core/src/queriesObserver.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export interface QueriesObserverOptions<
3030
TCombinedResult = Array<QueryObserverResult>,
3131
> {
3232
combine?: CombineFn<TCombinedResult>
33+
/**
34+
* Set this to `false` to disable structural sharing between query results.
35+
* Only applies when `combine` is provided.
36+
* Defaults to `true`.
37+
*/
38+
structuralSharing?: boolean
3339
}
3440

3541
export class QueriesObserver<
@@ -172,6 +178,7 @@ export class QueriesObserver<
172178
getOptimisticResult(
173179
queries: Array<QueryObserverOptions>,
174180
combine: CombineFn<TCombinedResult> | undefined,
181+
structuralSharing: boolean | undefined,
175182
): [
176183
rawResult: Array<QueryObserverResult>,
177184
combineResult: (r?: Array<QueryObserverResult>) => TCombinedResult,
@@ -188,7 +195,12 @@ export class QueriesObserver<
188195
return [
189196
result,
190197
(r?: Array<QueryObserverResult>) => {
191-
return this.#combineResult(r ?? result, combine, queryHashes)
198+
return this.#combineResult(
199+
r ?? result,
200+
combine,
201+
structuralSharing,
202+
queryHashes,
203+
)
192204
},
193205
() => {
194206
return this.#trackResult(result, matches)
@@ -216,6 +228,7 @@ export class QueriesObserver<
216228
#combineResult(
217229
input: Array<QueryObserverResult>,
218230
combine: CombineFn<TCombinedResult> | undefined,
231+
structuralSharing: boolean | undefined = true,
219232
queryHashes?: Array<string>,
220233
): TCombinedResult {
221234
if (combine) {
@@ -238,10 +251,12 @@ export class QueriesObserver<
238251
if (queryHashes !== undefined) {
239252
this.#lastQueryHashes = queryHashes
240253
}
241-
this.#combinedResult = replaceEqualDeep(
242-
this.#combinedResult,
243-
combine(input),
244-
)
254+
255+
const combined = combine(input)
256+
257+
this.#combinedResult = structuralSharing
258+
? replaceEqualDeep(this.#combinedResult, combined)
259+
: combined
245260
}
246261

247262
return this.#combinedResult
@@ -296,7 +311,11 @@ export class QueriesObserver<
296311
if (this.hasListeners()) {
297312
const previousResult = this.#combinedResult
298313
const newTracked = this.#trackResult(this.#result, this.#observerMatches)
299-
const newResult = this.#combineResult(newTracked, this.#options?.combine)
314+
const newResult = this.#combineResult(
315+
newTracked,
316+
this.#options?.combine,
317+
this.#options?.structuralSharing,
318+
)
300319

301320
if (previousResult !== newResult) {
302321
notifyManager.batch(() => {

packages/react-query/src/useQueries.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ export function useQueries<
217217
| readonly [...QueriesOptions<T>]
218218
| readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }]
219219
combine?: (result: QueriesResults<T>) => TCombinedResult
220+
/**
221+
* Set this to `false` to disable structural sharing between query results.
222+
* Only applies when `combine` is provided.
223+
* Defaults to `true`.
224+
*/
225+
structuralSharing?: boolean
220226
subscribed?: boolean
221227
},
222228
queryClient?: QueryClient,
@@ -264,6 +270,7 @@ export function useQueries<
264270
observer.getOptimisticResult(
265271
defaultedQueries,
266272
(options as QueriesObserverOptions<TCombinedResult>).combine,
273+
options.structuralSharing,
267274
)
268275

269276
const shouldSubscribe = !isRestoring && options.subscribed !== false

packages/solid-query/src/useQueries.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ export function useQueries<
193193
| readonly [...QueriesOptions<T>]
194194
| readonly [...{ [K in keyof T]: GetOptions<T[K]> }]
195195
combine?: (result: QueriesResults<T>) => TCombinedResult
196+
/**
197+
* Set this to `false` to disable structural sharing between query results.
198+
* Only applies when `combine` is provided.
199+
* Defaults to `true`.
200+
*/
201+
structuralSharing?: boolean
196202
}>,
197203
queryClient?: Accessor<QueryClient>,
198204
): TCombinedResult {
@@ -218,6 +224,7 @@ export function useQueries<
218224
queriesOptions().combine
219225
? ({
220226
combine: queriesOptions().combine,
227+
structuralSharing: queriesOptions().structuralSharing,
221228
} as QueriesObserverOptions<TCombinedResult>)
222229
: undefined,
223230
)
@@ -226,6 +233,8 @@ export function useQueries<
226233
observer.getOptimisticResult(
227234
defaultedQueries(),
228235
(queriesOptions() as QueriesObserverOptions<TCombinedResult>).combine,
236+
(queriesOptions() as QueriesObserverOptions<TCombinedResult>)
237+
.structuralSharing,
229238
)[1](),
230239
)
231240

@@ -238,6 +247,8 @@ export function useQueries<
238247
defaultedQueries(),
239248
(queriesOptions() as QueriesObserverOptions<TCombinedResult>)
240249
.combine,
250+
(queriesOptions() as QueriesObserverOptions<TCombinedResult>)
251+
.structuralSharing,
241252
)[1](),
242253
),
243254
),
@@ -306,6 +317,7 @@ export function useQueries<
306317
queriesOptions().combine
307318
? ({
308319
combine: queriesOptions().combine,
320+
structuralSharing: queriesOptions().structuralSharing,
309321
} as QueriesObserverOptions<TCombinedResult>)
310322
: undefined,
311323
)
@@ -317,6 +329,7 @@ export function useQueries<
317329
queriesOptions().combine
318330
? ({
319331
combine: queriesOptions().combine,
332+
structuralSharing: queriesOptions().structuralSharing,
320333
} as QueriesObserverOptions<TCombinedResult>)
321334
: undefined,
322335
)

packages/svelte-query/src/createQueries.svelte.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,20 @@ export function createQueries<
197197
...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries<T[K]> },
198198
]
199199
combine?: (result: QueriesResults<T>) => TCombinedResult
200+
/**
201+
* Set this to `false` to disable structural sharing between query results.
202+
* Only applies when `combine` is provided.
203+
* Defaults to `true`.
204+
*/
205+
structuralSharing?: boolean
200206
}>,
201207
queryClient?: Accessor<QueryClient>,
202208
): TCombinedResult {
203209
const client = $derived(useQueryClient(queryClient?.()))
204210
const isRestoring = useIsRestoring()
205211

206-
const { queries, combine } = $derived.by(createQueriesOptions)
212+
const { queries, ...derivedCreateQueriesOptions } =
213+
$derived.by(createQueriesOptions)
207214
const resolvedQueryOptions = $derived(
208215
queries.map((opts) => {
209216
const resolvedOptions = client.defaultQueryOptions(opts)
@@ -220,14 +227,15 @@ export function createQueries<
220227
new QueriesObserver<TCombinedResult>(
221228
client,
222229
resolvedQueryOptions,
223-
combine as QueriesObserverOptions<TCombinedResult>,
230+
derivedCreateQueriesOptions as QueriesObserverOptions<TCombinedResult>,
224231
),
225232
)
226233

227234
function createResult() {
228235
const [_, getCombinedResult, trackResult] = observer.getOptimisticResult(
229236
resolvedQueryOptions,
230-
combine as QueriesObserverOptions<TCombinedResult>['combine'],
237+
derivedCreateQueriesOptions.combine as QueriesObserverOptions<TCombinedResult>['combine'],
238+
derivedCreateQueriesOptions.structuralSharing,
231239
)
232240
return getCombinedResult(trackResult())
233241
}
@@ -244,9 +252,10 @@ export function createQueries<
244252
})
245253

246254
$effect.pre(() => {
247-
observer.setQueries(resolvedQueryOptions, {
248-
combine,
249-
} as QueriesObserverOptions<TCombinedResult>)
255+
observer.setQueries(
256+
resolvedQueryOptions,
257+
derivedCreateQueriesOptions as QueriesObserverOptions<TCombinedResult>,
258+
)
250259
update(createResult())
251260
})
252261

packages/vue-query/src/useQueries.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,12 @@ export function useQueries<
248248
]
249249
>
250250
combine?: (result: UseQueriesResults<T>) => TCombinedResult
251+
/**
252+
* Set this to `false` to disable structural sharing between query results.
253+
* Only applies when `combine` is provided.
254+
* Defaults to `true`.
255+
*/
256+
structuralSharing?: boolean
251257
},
252258
queryClient?: QueryClient,
253259
): Readonly<Ref<TCombinedResult>> {
@@ -296,6 +302,7 @@ export function useQueries<
296302
const [results, getCombinedResult] = observer.getOptimisticResult(
297303
defaultedQueries.value,
298304
(options as QueriesObserverOptions<TCombinedResult>).combine,
305+
options.structuralSharing,
299306
)
300307

301308
return getCombinedResult(
@@ -306,6 +313,7 @@ export function useQueries<
306313
const [{ [index]: query }] = observer.getOptimisticResult(
307314
defaultedQueries.value,
308315
(options as QueriesObserverOptions<TCombinedResult>).combine,
316+
options.structuralSharing,
309317
)
310318

311319
return query!.refetch(...args)

0 commit comments

Comments
 (0)