From 34dc1f7f599ab16196adc5691eff5ed17f48aaa1 Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Fri, 6 Feb 2026 21:35:17 +0100 Subject: [PATCH 1/9] 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`. --- .changeset/silver-coins-mix.md | 10 +++++++ docs/framework/react/reference/useQueries.md | 5 +++- .../src/inject-queries.ts | 8 ++++++ .../src/__tests__/queriesObserver.test.tsx | 7 +++++ packages/query-core/src/queriesObserver.ts | 26 +++++++++++++++---- packages/react-query/src/useQueries.ts | 7 +++++ packages/solid-query/src/useQueries.ts | 23 +++++++++------- .../svelte-query/src/createQueries.svelte.ts | 21 ++++++++++----- packages/vue-query/src/useQueries.ts | 8 ++++++ 9 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 .changeset/silver-coins-mix.md diff --git a/.changeset/silver-coins-mix.md b/.changeset/silver-coins-mix.md new file mode 100644 index 00000000000..d02adb65ee4 --- /dev/null +++ b/.changeset/silver-coins-mix.md @@ -0,0 +1,10 @@ +--- +'@tanstack/angular-query-experimental': minor +'@tanstack/svelte-query': minor +'@tanstack/react-query': minor +'@tanstack/solid-query': minor +'@tanstack/query-core': minor +'@tanstack/vue-query': minor +--- + +feat(query-core): Allow disabling structuralSharing for useQueries. diff --git a/docs/framework/react/reference/useQueries.md b/docs/framework/react/reference/useQueries.md index 1048b532d29..51afab2d513 100644 --- a/docs/framework/react/reference/useQueries.md +++ b/docs/framework/react/reference/useQueries.md @@ -24,6 +24,9 @@ The `useQueries` hook accepts an options object with a **queries** key whose val - 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. +- `structuralSharing?: boolean` + - Set this to `false` to disable structural sharing between query results when `combine` is provided. + - Defaults to `true`. > 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. @@ -37,7 +40,7 @@ The `useQueries` hook returns an array with all the query results. The order ret ## 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. +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`. ```tsx const ids = [1, 2, 3] diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts index d61a937a3c2..f7ec4789dc8 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -211,6 +211,12 @@ export interface InjectQueriesOptions< ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries }, ] combine?: (result: QueriesResults) => TCombinedResult + /** + * Set this to `false` to disable structural sharing between query results. + * Only applies when `combine` is provided. + * Defaults to `true`. + */ + structuralSharing?: boolean } /** @@ -271,6 +277,8 @@ export function injectQueries< observerSignal().getOptimisticResult( defaultedQueries(), (optionsSignal() as QueriesObserverOptions).combine, + (optionsSignal() as QueriesObserverOptions) + .structuralSharing, ), ) diff --git a/packages/query-core/src/__tests__/queriesObserver.test.tsx b/packages/query-core/src/__tests__/queriesObserver.test.tsx index 850ffa77146..46edb1f2544 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -343,6 +343,7 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, ], undefined, + undefined, )[0], ) @@ -457,6 +458,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], combine, + undefined, ) const initialCombined = getInitialCombined(initialRaw) @@ -469,6 +471,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( newQueries, combine, + undefined, ) const newCombined = getNewCombined(newRaw) @@ -580,6 +583,7 @@ describe('queriesObserver', () => { { queryKey: key2, queryFn: queryFn2 }, ], combine, + undefined, ) const initialCombined = getInitialCombined(initialRaw) @@ -589,6 +593,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( newQueries, combine, + undefined, ) const newCombined = getNewCombined(newRaw) @@ -616,6 +621,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], combine, + undefined, ) const initialCombined = getInitialCombined(initialRaw) @@ -624,6 +630,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( [{ queryKey: key2, queryFn: queryFn2 }], combine, + undefined, ) const newCombined = getNewCombined(newRaw) diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index 0f0cc7a440b..bb2c559bb26 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -30,6 +30,12 @@ export interface QueriesObserverOptions< TCombinedResult = Array, > { combine?: CombineFn + /** + * Set this to `false` to disable structural sharing between query results. + * Only applies when `combine` is provided. + * Defaults to `true`. + */ + structuralSharing?: boolean } export class QueriesObserver< @@ -172,6 +178,7 @@ export class QueriesObserver< getOptimisticResult( queries: Array, combine: CombineFn | undefined, + structuralSharing: boolean | undefined, ): [ rawResult: Array, combineResult: (r?: Array) => TCombinedResult, @@ -188,7 +195,12 @@ export class QueriesObserver< return [ result, (r?: Array) => { - return this.#combineResult(r ?? result, combine, queryHashes) + return this.#combineResult( + r ?? result, + combine, + structuralSharing, + queryHashes, + ) }, () => { return this.#trackResult(result, matches) @@ -221,6 +233,7 @@ export class QueriesObserver< #combineResult( input: Array, combine: CombineFn | undefined, + structuralSharing: boolean | undefined = true, queryHashes?: Array, ): TCombinedResult { if (combine) { @@ -242,10 +255,12 @@ export class QueriesObserver< if (queryHashes !== undefined) { this.#lastQueryHashes = queryHashes } - this.#combinedResult = replaceEqualDeep( - this.#combinedResult, - combine(input), - ) + + const combined = combine(input) + + this.#combinedResult = structuralSharing + ? replaceEqualDeep(this.#combinedResult, combined) + : combined } return this.#combinedResult @@ -316,6 +331,7 @@ export class QueriesObserver< : this.#combineResult( this.#trackResult(this.#result, this.#observerMatches), this.#options?.combine, + this.#options?.structuralSharing, ) if (shouldSkipCombine || previousResult !== newResult) { diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 437718a988f..d3134d2cb63 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -216,6 +216,12 @@ export function useQueries< | readonly [...QueriesOptions] | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries }] combine?: (result: QueriesResults) => TCombinedResult + /** + * Set this to `false` to disable structural sharing between query results. + * Only applies when `combine` is provided. + * Defaults to `true`. + */ + structuralSharing?: boolean subscribed?: boolean }, queryClient?: QueryClient, @@ -266,6 +272,7 @@ export function useQueries< observer.getOptimisticResult( defaultedQueries, (options as QueriesObserverOptions).combine, + options.structuralSharing, ) const shouldSubscribe = !isRestoring && subscribed diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index 9bf909634b2..049dc06e564 100644 --- a/packages/solid-query/src/useQueries.ts +++ b/packages/solid-query/src/useQueries.ts @@ -193,6 +193,12 @@ export function useQueries< | readonly [...QueriesOptions] | readonly [...{ [K in keyof T]: GetOptions }] combine?: (result: QueriesResults) => TCombinedResult + /** + * Set this to `false` to disable structural sharing between query results. + * Only applies when `combine` is provided. + * Defaults to `true`. + */ + structuralSharing?: boolean }>, queryClient?: Accessor, ): TCombinedResult { @@ -219,6 +225,7 @@ export function useQueries< queriesOptions().combine ? ({ combine: queriesOptions().combine, + structuralSharing: queriesOptions().structuralSharing, } as QueriesObserverOptions) : undefined, ) @@ -227,6 +234,8 @@ export function useQueries< observer.getOptimisticResult( defaultedQueries(), (queriesOptions() as QueriesObserverOptions).combine, + (queriesOptions() as QueriesObserverOptions) + .structuralSharing, )[1](), ) @@ -239,6 +248,8 @@ export function useQueries< defaultedQueries(), (queriesOptions() as QueriesObserverOptions) .combine, + (queriesOptions() as QueriesObserverOptions) + .structuralSharing, )[1](), ), ), @@ -304,22 +315,14 @@ export function useQueries< onMount(() => { observer.setQueries( defaultedQueries(), - queriesOptions().combine - ? ({ - combine: queriesOptions().combine, - } as QueriesObserverOptions) - : undefined, + queriesOptions() as QueriesObserverOptions, ) }) createComputed(() => { observer.setQueries( defaultedQueries(), - queriesOptions().combine - ? ({ - combine: queriesOptions().combine, - } as QueriesObserverOptions) - : undefined, + queriesOptions() as QueriesObserverOptions, ) }) diff --git a/packages/svelte-query/src/createQueries.svelte.ts b/packages/svelte-query/src/createQueries.svelte.ts index 7f46ddf33ab..d37dac6c74a 100644 --- a/packages/svelte-query/src/createQueries.svelte.ts +++ b/packages/svelte-query/src/createQueries.svelte.ts @@ -197,13 +197,20 @@ export function createQueries< ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries }, ] combine?: (result: QueriesResults) => TCombinedResult + /** + * Set this to `false` to disable structural sharing between query results. + * Only applies when `combine` is provided. + * Defaults to `true`. + */ + structuralSharing?: boolean }>, queryClient?: Accessor, ): TCombinedResult { const client = $derived(useQueryClient(queryClient?.())) const isRestoring = useIsRestoring() - const { queries, combine } = $derived.by(createQueriesOptions) + const { queries, ...derivedCreateQueriesOptions } = + $derived.by(createQueriesOptions) const resolvedQueryOptions = $derived( queries.map((opts) => { const resolvedOptions = client.defaultQueryOptions(opts) @@ -220,14 +227,15 @@ export function createQueries< new QueriesObserver( client, resolvedQueryOptions, - combine as QueriesObserverOptions, + derivedCreateQueriesOptions as QueriesObserverOptions, ), ) function createResult() { const [_, getCombinedResult, trackResult] = observer.getOptimisticResult( resolvedQueryOptions, - combine as QueriesObserverOptions['combine'], + derivedCreateQueriesOptions.combine as QueriesObserverOptions['combine'], + derivedCreateQueriesOptions.structuralSharing, ) return getCombinedResult(trackResult()) } @@ -244,9 +252,10 @@ export function createQueries< }) $effect.pre(() => { - observer.setQueries(resolvedQueryOptions, { - combine, - } as QueriesObserverOptions) + observer.setQueries( + resolvedQueryOptions, + derivedCreateQueriesOptions as QueriesObserverOptions, + ) update(createResult()) }) diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index 43066a54846..b1ca799234a 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -248,6 +248,12 @@ export function useQueries< ] > combine?: (result: UseQueriesResults) => TCombinedResult + /** + * Set this to `false` to disable structural sharing between query results. + * Only applies when `combine` is provided. + * Defaults to `true`. + */ + structuralSharing?: boolean }, queryClient?: QueryClient, ): Readonly> { @@ -296,6 +302,7 @@ export function useQueries< const [results, getCombinedResult] = observer.getOptimisticResult( defaultedQueries.value, (options as QueriesObserverOptions).combine, + options.structuralSharing, ) return getCombinedResult( @@ -306,6 +313,7 @@ export function useQueries< const [{ [index]: query }] = observer.getOptimisticResult( defaultedQueries.value, (options as QueriesObserverOptions).combine, + options.structuralSharing, ) return query!.refetch(...args) From 6fba7ad463f479309fb5cc3bb10052b7ff61d3c1 Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Tue, 10 Feb 2026 19:31:41 +0100 Subject: [PATCH 2/9] Add structuralSharing to useQueries in the preact-adapter. --- packages/preact-query/src/useQueries.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/preact-query/src/useQueries.ts b/packages/preact-query/src/useQueries.ts index dac252d0109..54529a90694 100644 --- a/packages/preact-query/src/useQueries.ts +++ b/packages/preact-query/src/useQueries.ts @@ -216,6 +216,12 @@ export function useQueries< | readonly [...QueriesOptions] | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries }] combine?: (result: QueriesResults) => TCombinedResult + /** + * Set this to `false` to disable structural sharing between query results. + * Only applies when `combine` is provided. + * Defaults to `true`. + */ + structuralSharing?: boolean subscribed?: boolean }, queryClient?: QueryClient, @@ -263,6 +269,7 @@ export function useQueries< observer.getOptimisticResult( defaultedQueries, (options as QueriesObserverOptions).combine, + options.structuralSharing, ) const shouldSubscribe = !isRestoring && options.subscribed !== false From e75aec2f7089e88bb5bb5bff41af6c748536f3b3 Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Tue, 10 Feb 2026 19:40:09 +0100 Subject: [PATCH 3/9] Refactor structuralSharing for useQueries to accept a function. Match the useQuery logic and allow a function to override the structural sharing. --- docs/framework/react/reference/useQueries.md | 8 +++-- .../src/inject-queries.ts | 5 +++- packages/preact-query/src/useQueries.ts | 5 +++- packages/query-core/src/queriesObserver.ts | 29 +++++++++++++++---- packages/react-query/src/useQueries.ts | 5 +++- packages/solid-query/src/useQueries.ts | 5 +++- .../svelte-query/src/createQueries.svelte.ts | 5 +++- packages/vue-query/src/useQueries.ts | 5 +++- 8 files changed, 52 insertions(+), 15 deletions(-) diff --git a/docs/framework/react/reference/useQueries.md b/docs/framework/react/reference/useQueries.md index 51afab2d513..31cabe9008d 100644 --- a/docs/framework/react/reference/useQueries.md +++ b/docs/framework/react/reference/useQueries.md @@ -24,9 +24,11 @@ The `useQueries` hook accepts an options object with a **queries** key whose val - 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. -- `structuralSharing?: boolean` - - Set this to `false` to disable structural sharing between query results when `combine` is provided. +- `structuralSharing?: boolean | ((oldData: unknown | undefined, newData: unknown) => unknown)` + - Optional - Defaults to `true`. + - If set to false, structural sharing between query results will be disabled. + - If set to a function, the old and new data values will be passed through this function, which should combine them into resolved data for the query. This way, you can retain references from the old data to improve performance even when that data contains non-serializable values. > 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. @@ -40,7 +42,7 @@ The `useQueries` hook returns an array with all the query results. The order ret ## 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. If you want to disable structural sharing for the combined result, you can set the `structuralSharing` option to `false`. +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`, or provide a custom function to implement your own structural sharing logic. ```tsx const ids = [1, 2, 3] diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts index f7ec4789dc8..ff0f79a4daf 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -213,10 +213,13 @@ export interface InjectQueriesOptions< combine?: (result: QueriesResults) => TCombinedResult /** * Set this to `false` to disable structural sharing between query results. + * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Only applies when `combine` is provided. * Defaults to `true`. */ - structuralSharing?: boolean + structuralSharing?: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) } /** diff --git a/packages/preact-query/src/useQueries.ts b/packages/preact-query/src/useQueries.ts index 54529a90694..696c519d240 100644 --- a/packages/preact-query/src/useQueries.ts +++ b/packages/preact-query/src/useQueries.ts @@ -218,10 +218,13 @@ export function useQueries< combine?: (result: QueriesResults) => TCombinedResult /** * Set this to `false` to disable structural sharing between query results. + * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Only applies when `combine` is provided. * Defaults to `true`. */ - structuralSharing?: boolean + structuralSharing?: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) subscribed?: boolean }, queryClient?: QueryClient, diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index bb2c559bb26..04186e6a041 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -32,10 +32,13 @@ export interface QueriesObserverOptions< combine?: CombineFn /** * Set this to `false` to disable structural sharing between query results. + * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Only applies when `combine` is provided. * Defaults to `true`. */ - structuralSharing?: boolean + structuralSharing?: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) } export class QueriesObserver< @@ -178,7 +181,10 @@ export class QueriesObserver< getOptimisticResult( queries: Array, combine: CombineFn | undefined, - structuralSharing: boolean | undefined, + structuralSharing: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) + | undefined, ): [ rawResult: Array, combineResult: (r?: Array) => TCombinedResult, @@ -233,7 +239,10 @@ export class QueriesObserver< #combineResult( input: Array, combine: CombineFn | undefined, - structuralSharing: boolean | undefined = true, + structuralSharing: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) + | undefined = true, queryHashes?: Array, ): TCombinedResult { if (combine) { @@ -258,9 +267,17 @@ export class QueriesObserver< const combined = combine(input) - this.#combinedResult = structuralSharing - ? replaceEqualDeep(this.#combinedResult, combined) - : combined + if (typeof structuralSharing === 'function') { + this.#combinedResult = structuralSharing( + this.#combinedResult, + combined, + ) as TCombinedResult + } else { + this.#combinedResult = + structuralSharing !== false + ? replaceEqualDeep(this.#combinedResult, combined) + : combined + } } return this.#combinedResult diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index d3134d2cb63..db5123e8862 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -218,10 +218,13 @@ export function useQueries< combine?: (result: QueriesResults) => TCombinedResult /** * Set this to `false` to disable structural sharing between query results. + * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Only applies when `combine` is provided. * Defaults to `true`. */ - structuralSharing?: boolean + structuralSharing?: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) subscribed?: boolean }, queryClient?: QueryClient, diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index 049dc06e564..de67859b22d 100644 --- a/packages/solid-query/src/useQueries.ts +++ b/packages/solid-query/src/useQueries.ts @@ -195,10 +195,13 @@ export function useQueries< combine?: (result: QueriesResults) => TCombinedResult /** * Set this to `false` to disable structural sharing between query results. + * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Only applies when `combine` is provided. * Defaults to `true`. */ - structuralSharing?: boolean + structuralSharing?: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) }>, queryClient?: Accessor, ): TCombinedResult { diff --git a/packages/svelte-query/src/createQueries.svelte.ts b/packages/svelte-query/src/createQueries.svelte.ts index d37dac6c74a..66d7a91bbee 100644 --- a/packages/svelte-query/src/createQueries.svelte.ts +++ b/packages/svelte-query/src/createQueries.svelte.ts @@ -199,10 +199,13 @@ export function createQueries< combine?: (result: QueriesResults) => TCombinedResult /** * Set this to `false` to disable structural sharing between query results. + * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Only applies when `combine` is provided. * Defaults to `true`. */ - structuralSharing?: boolean + structuralSharing?: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) }>, queryClient?: Accessor, ): TCombinedResult { diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index b1ca799234a..bdcb851ed7f 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -250,10 +250,13 @@ export function useQueries< combine?: (result: UseQueriesResults) => TCombinedResult /** * Set this to `false` to disable structural sharing between query results. + * Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic. * Only applies when `combine` is provided. * Defaults to `true`. */ - structuralSharing?: boolean + structuralSharing?: + | boolean + | ((oldData: unknown | undefined, newData: unknown) => unknown) }, queryClient?: QueryClient, ): Readonly> { From b1ace58f074b03e8df53e4fb78d07b0c26503f3f Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Tue, 10 Feb 2026 19:54:24 +0100 Subject: [PATCH 4/9] Add tests for the structuralSharing option in QueriesObserver. --- .../src/__tests__/queriesObserver.test.tsx | 288 +++++++++++++++++- 1 file changed, 283 insertions(+), 5 deletions(-) diff --git a/packages/query-core/src/__tests__/queriesObserver.test.tsx b/packages/query-core/src/__tests__/queriesObserver.test.tsx index 46edb1f2544..b118f15ed74 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -499,6 +499,7 @@ describe('queriesObserver', () => { const [rawResult, getCombinedResult] = observer.getOptimisticResult( [query], combine, + undefined, ) expect(getCombinedResult(rawResult)).toEqual(['data']) expect(combine).toHaveBeenCalledTimes(1) @@ -532,6 +533,7 @@ describe('queriesObserver', () => { const [rawResult, getCombinedResult] = observer.getOptimisticResult( [query], combine, + undefined, ) expect(getCombinedResult(rawResult)).toEqual(['data']) expect(combine).toHaveBeenCalledTimes(1) @@ -661,16 +663,211 @@ describe('queriesObserver', () => { { combine: combine1 }, ) - const [raw1, getCombined1] = observer.getOptimisticResult(queries, combine1) + const [raw1, getCombined1] = observer.getOptimisticResult( + queries, + combine1, + undefined, + ) const combined1 = getCombined1(raw1) - - const [raw2, getCombined2] = observer.getOptimisticResult(queries, combine2) + const [raw2, getCombined2] = observer.getOptimisticResult( + queries, + combine2, + undefined, + ) const combined2 = getCombined2(raw2) expect(combined1.total).toBe(2) expect(combined2.total).toBe(8) }) + it('should not use structural sharing when structuralSharing is false', () => { + const key1 = queryKey() + const queryFn1 = vi.fn().mockReturnValue(1) + + queryClient.setQueryData(key1, 'cached-1') + + // Create a combine function that returns a new object with a nested array + const nestedArray = ['a', 'b', 'c'] + const combine = vi.fn((_results: Array) => ({ + nested: nestedArray, + })) + + const observer = new QueriesObserver<{ + nested: Array + }>(queryClient, [{ queryKey: key1, queryFn: queryFn1 }], { + combine, + structuralSharing: false, + }) + + const [initialRaw, getInitialCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine, + false, + ) + const initialCombined = getInitialCombined(initialRaw) + + // Create a new combine function reference to trigger re-combine + // but with the same nested array content + const combine2 = vi.fn((_results: Array) => ({ + nested: ['a', 'b', 'c'], // Same content, different reference + })) + + const [newRaw, getNewCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine2, + false, + ) + const newCombined = getNewCombined(newRaw) + + // With structuralSharing: false, even though the nested array has the same content, + // the reference should NOT be preserved (no replaceEqualDeep optimization) + expect(newCombined.nested).toEqual(initialCombined.nested) + expect(newCombined.nested).not.toBe(initialCombined.nested) + }) + + it('should use structural sharing when structuralSharing is true', () => { + const key1 = queryKey() + const queryFn1 = vi.fn().mockReturnValue(1) + + queryClient.setQueryData(key1, 'cached-1') + + // Create a combine function that returns a new object with a nested array + const combine = vi.fn((_results: Array) => ({ + nested: ['a', 'b', 'c'], + })) + + const observer = new QueriesObserver<{ + nested: Array + }>(queryClient, [{ queryKey: key1, queryFn: queryFn1 }], { + combine, + structuralSharing: true, + }) + + const [initialRaw, getInitialCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine, + true, + ) + const initialCombined = getInitialCombined(initialRaw) + + // Create a new combine function reference to trigger re-combine + // but with the same nested array content + const combine2 = vi.fn((_results: Array) => ({ + nested: ['a', 'b', 'c'], // Same content, different reference + })) + + const [newRaw, getNewCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine2, + true, + ) + const newCombined = getNewCombined(newRaw) + + // With structuralSharing: true, replaceEqualDeep should preserve the reference + // since the nested array has the same content + expect(newCombined.nested).toEqual(initialCombined.nested) + expect(newCombined.nested).toBe(initialCombined.nested) + }) + + it('should use custom structuralSharing function when provided', () => { + const combine = vi.fn((results: Array) => ({ + count: results.length, + data: results.map((r) => r.data), + })) + + const customStructuralSharing = vi.fn( + (_oldData: unknown, newData: unknown) => { + // Custom logic: always return the new data but with a marker + return { ...(newData as object), customShared: true } + }, + ) + + const key1 = queryKey() + const queryFn1 = vi.fn().mockReturnValue(1) + + queryClient.setQueryData(key1, 'cached-1') + + const observer = new QueriesObserver<{ + count: number + data: Array + customShared?: boolean + }>(queryClient, [{ queryKey: key1, queryFn: queryFn1 }], { + combine, + structuralSharing: customStructuralSharing, + }) + + const [initialRaw, getInitialCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine, + customStructuralSharing, + ) + const initialCombined = getInitialCombined(initialRaw) + + expect(initialCombined.count).toBe(1) + expect(initialCombined.customShared).toBe(true) + expect(customStructuralSharing).toHaveBeenCalledTimes(1) + }) + + it('should pass old and new data to custom structuralSharing function', () => { + const combine = vi.fn((results: Array) => ({ + count: results.length, + data: results.map((r) => r.data), + })) + + const customStructuralSharing = vi.fn( + (_oldData: unknown, newData: unknown) => { + // Return new data with reference to old data for testing + return newData + }, + ) + + const key1 = queryKey() + const queryFn1 = vi.fn().mockReturnValue(1) + + queryClient.setQueryData(key1, 'cached-1') + + const observer = new QueriesObserver<{ + count: number + data: Array + }>(queryClient, [{ queryKey: key1, queryFn: queryFn1 }], { + combine, + structuralSharing: customStructuralSharing, + }) + + const [initialRaw, getInitialCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine, + customStructuralSharing, + ) + const initialCombined = getInitialCombined(initialRaw) + + expect(initialCombined.count).toBe(1) + + const secondKey = queryKey() + const [newRaw, getNewCombined] = observer.getOptimisticResult( + [ + { queryKey: key1, queryFn: queryFn1 }, + { queryKey: secondKey, queryFn: () => 2 }, + ], + combine, + customStructuralSharing, + ) + const newCombined = getNewCombined(newRaw) + + expect(newCombined.count).toBe(2) + expect(customStructuralSharing).toHaveBeenCalledTimes(2) + expect(customStructuralSharing).toHaveBeenNthCalledWith( + 1, + undefined, + expect.objectContaining({ count: 1 }), + ) + expect(customStructuralSharing).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ count: 1 }), + expect.objectContaining({ count: 2 }), + ) + }) + it('should use fallback result when combineResult is called without raw argument', () => { const combine = vi.fn((results: Array) => ({ count: results.length, @@ -688,6 +885,7 @@ describe('queriesObserver', () => { const [, getCombined] = observer.getOptimisticResult( [{ queryKey: key, queryFn }], combine, + undefined, ) const combined = getCombined() @@ -707,6 +905,7 @@ describe('queriesObserver', () => { const [, , trackResult] = observer.getOptimisticResult( [{ queryKey: key, queryFn, notifyOnChangeProps: ['data'] }], undefined, + undefined, ) const trackedResults = trackResult() @@ -734,10 +933,18 @@ describe('queriesObserver', () => { { combine }, ) - const [raw1, getCombined1] = observer.getOptimisticResult(queries, combine) + const [raw1, getCombined1] = observer.getOptimisticResult( + queries, + combine, + undefined, + ) const combined1 = getCombined1(raw1) - const [raw2, getCombined2] = observer.getOptimisticResult(queries, combine) + const [raw2, getCombined2] = observer.getOptimisticResult( + queries, + combine, + undefined, + ) const combined2 = getCombined2(raw2) // Same combine, same queries → cached result returned @@ -766,12 +973,14 @@ describe('queriesObserver', () => { const [raw1, getCombined1] = observer.getOptimisticResult( queries, combine, + undefined, ) getCombined1(raw1) const [raw2, getCombined2] = observer.getOptimisticResult( queries, combine, + undefined, ) getCombined2(raw2) @@ -798,6 +1007,7 @@ describe('queriesObserver', () => { { queryKey: key2, queryFn: queryFn2 }, ], undefined, + undefined, ) const trackedResults = trackResult() @@ -858,4 +1068,72 @@ describe('queriesObserver', () => { { status: 'success', data: 3 }, ]) }) + + it('should retain references with custom structuralSharing function', () => { + // This test verifies that a custom structuralSharing function can retain references + const existingArray = [1, 2, 3] + + const combine = vi.fn((results: Array) => ({ + count: results.length, + data: results.map((r) => r.data), + existingArray, + })) + + const customStructuralSharing = vi.fn( + (oldData: unknown, newData: unknown) => { + const oldTyped = oldData as + | { existingArray?: Array } + | undefined + const newTyped = newData as { existingArray: Array } + // Retain the existingArray reference from old data if it deeply equals + if ( + oldTyped?.existingArray && + JSON.stringify(oldTyped.existingArray) === + JSON.stringify(newTyped.existingArray) + ) { + return { ...newTyped, existingArray: oldTyped.existingArray } + } + return newTyped + }, + ) + + const key1 = queryKey() + const queryFn1 = vi.fn().mockReturnValue(1) + + queryClient.setQueryData(key1, 'cached-1') + + const observer = new QueriesObserver<{ + count: number + data: Array + existingArray: Array + }>(queryClient, [{ queryKey: key1, queryFn: queryFn1 }], { + combine, + structuralSharing: customStructuralSharing, + }) + + const [initialRaw, getInitialCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine, + customStructuralSharing, + ) + const initialCombined = getInitialCombined(initialRaw) + const initialArrayRef = initialCombined.existingArray + + // Trigger a re-combine by changing the combine function reference + const combine2 = vi.fn((results: Array) => ({ + count: results.length, + data: results.map((r) => r.data), + existingArray, // Same array content + })) + + const [newRaw, getNewCombined] = observer.getOptimisticResult( + [{ queryKey: key1, queryFn: queryFn1 }], + combine2, + customStructuralSharing, + ) + const newCombined = getNewCombined(newRaw) + + // The existingArray reference should be retained from the old data + expect(newCombined.existingArray).toBe(initialArrayRef) + }) }) From dda3beefc6cbe8e496d50227a37e7edcebcf17f5 Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Tue, 10 Feb 2026 19:55:32 +0100 Subject: [PATCH 5/9] Add preact adapter to changeset. --- .changeset/silver-coins-mix.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/silver-coins-mix.md b/.changeset/silver-coins-mix.md index d02adb65ee4..efc41ecea9f 100644 --- a/.changeset/silver-coins-mix.md +++ b/.changeset/silver-coins-mix.md @@ -2,6 +2,7 @@ '@tanstack/angular-query-experimental': minor '@tanstack/svelte-query': minor '@tanstack/react-query': minor +'@tanstack/preact-query': minor '@tanstack/solid-query': minor '@tanstack/query-core': minor '@tanstack/vue-query': minor From 1bf48d35018ae6ba354e63e8aefa8e5a2b1a7871 Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Tue, 10 Feb 2026 20:18:59 +0100 Subject: [PATCH 6/9] Fix docs nitpicks from coderabbit for structuralSharing for useQueries. --- docs/framework/react/reference/useQueries.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/framework/react/reference/useQueries.md b/docs/framework/react/reference/useQueries.md index 31cabe9008d..644536658b0 100644 --- a/docs/framework/react/reference/useQueries.md +++ b/docs/framework/react/reference/useQueries.md @@ -27,7 +27,8 @@ The `useQueries` hook accepts an options object with a **queries** key whose val - `structuralSharing?: boolean | ((oldData: unknown | undefined, newData: unknown) => unknown)` - Optional - Defaults to `true`. - - If set to false, structural sharing between query results will be disabled. + - Only applies when `combine` is provided. + - If set to `false`, structural sharing between query results will be disabled. - If set to a function, the old and new data values will be passed through this function, which should combine them into resolved data for the query. This way, you can retain references from the old data to improve performance even when that data contains non-serializable values. > 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. From 161c58bbb4c462d0acd9db7ddc10792ca93f0eff Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Mon, 2 Mar 2026 09:37:51 +0100 Subject: [PATCH 7/9] Use replaceData util to replace combined result. --- packages/query-core/src/queriesObserver.ts | 23 +++++++++------------- packages/query-core/src/utils.ts | 13 ++++++++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index 04186e6a041..b5c3a65bdb1 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -1,7 +1,7 @@ import { notifyManager } from './notifyManager' import { QueryObserver } from './queryObserver' import { Subscribable } from './subscribable' -import { replaceEqualDeep, shallowEqualObjects } from './utils' +import { replaceData, shallowEqualObjects } from './utils' import type { DefaultedQueryObserverOptions, QueryObserverOptions, @@ -265,19 +265,14 @@ export class QueriesObserver< this.#lastQueryHashes = queryHashes } - const combined = combine(input) - - if (typeof structuralSharing === 'function') { - this.#combinedResult = structuralSharing( - this.#combinedResult, - combined, - ) as TCombinedResult - } else { - this.#combinedResult = - structuralSharing !== false - ? replaceEqualDeep(this.#combinedResult, combined) - : combined - } + this.#combinedResult = replaceData( + this.#combinedResult, + combine(input), + { + structuralSharing, + queryHash: queryHashes, + }, + ) } return this.#combinedResult diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index f442ab86fdc..533e2c46b2b 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -401,7 +401,10 @@ export function sleep(timeout: number): Promise { export function replaceData< TData, - TOptions extends QueryOptions, + TOptions extends { + structuralSharing?: boolean | ((prev: unknown, data: unknown) => unknown) + queryHash?: string | Array + }, >(prevData: TData | undefined, data: TData, options: TOptions): TData { if (typeof options.structuralSharing === 'function') { return options.structuralSharing(prevData, data) as TData @@ -410,8 +413,14 @@ export function replaceData< try { return replaceEqualDeep(prevData, data) } catch (error) { + let hashInfo = '' + if (Array.isArray(options.queryHash)) { + hashInfo = `queryHashes: [${options.queryHash.join(', ')}]` + } else if (options.queryHash) { + hashInfo = `queryHash: ${options.queryHash}` + } console.error( - `Structural sharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${options.queryHash}]: ${error}`, + `Structural sharing requires data to be JSON serializable. To fix this, turn off structuralSharing or return JSON-serializable data from your queryFn. [${hashInfo}]: ${error}`, ) // Prevent the replaceEqualDeep from being called again down below. From 0526c6f1c45c6e30ca78968b488b2297671ff997 Mon Sep 17 00:00:00 2001 From: Dag Stuan Date: Mon, 2 Mar 2026 09:44:53 +0100 Subject: [PATCH 8/9] Pass complete queries options to getOptimisticResult. --- .../src/inject-queries.ts | 4 +- packages/preact-query/src/useQueries.ts | 3 +- .../src/__tests__/queriesObserver.test.tsx | 75 ++++++------------- packages/query-core/src/queriesObserver.ts | 28 ++----- packages/react-query/src/useQueries.ts | 3 +- packages/solid-query/src/useQueries.ts | 9 +-- .../svelte-query/src/createQueries.svelte.ts | 3 +- packages/vue-query/src/useQueries.ts | 6 +- 8 files changed, 39 insertions(+), 92 deletions(-) diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts index ff0f79a4daf..25c1923c6ef 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -279,9 +279,7 @@ export function injectQueries< const optimisticResultSignal = computed(() => observerSignal().getOptimisticResult( defaultedQueries(), - (optionsSignal() as QueriesObserverOptions).combine, - (optionsSignal() as QueriesObserverOptions) - .structuralSharing, + optionsSignal() as QueriesObserverOptions, ), ) diff --git a/packages/preact-query/src/useQueries.ts b/packages/preact-query/src/useQueries.ts index 696c519d240..54ab373d8a5 100644 --- a/packages/preact-query/src/useQueries.ts +++ b/packages/preact-query/src/useQueries.ts @@ -271,8 +271,7 @@ export function useQueries< const [optimisticResult, getCombinedResult, trackResult] = observer.getOptimisticResult( defaultedQueries, - (options as QueriesObserverOptions).combine, - options.structuralSharing, + options as QueriesObserverOptions, ) const shouldSubscribe = !isRestoring && options.subscribed !== false diff --git a/packages/query-core/src/__tests__/queriesObserver.test.tsx b/packages/query-core/src/__tests__/queriesObserver.test.tsx index b118f15ed74..f8361d9bfa0 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -343,7 +343,6 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, ], undefined, - undefined, )[0], ) @@ -457,8 +456,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, - undefined, + { combine }, ) const initialCombined = getInitialCombined(initialRaw) @@ -470,8 +468,7 @@ describe('queriesObserver', () => { ] const [newRaw, getNewCombined] = observer.getOptimisticResult( newQueries, - combine, - undefined, + { combine }, ) const newCombined = getNewCombined(newRaw) @@ -498,8 +495,7 @@ describe('queriesObserver', () => { const [rawResult, getCombinedResult] = observer.getOptimisticResult( [query], - combine, - undefined, + { combine }, ) expect(getCombinedResult(rawResult)).toEqual(['data']) expect(combine).toHaveBeenCalledTimes(1) @@ -532,8 +528,7 @@ describe('queriesObserver', () => { const [rawResult, getCombinedResult] = observer.getOptimisticResult( [query], - combine, - undefined, + { combine }, ) expect(getCombinedResult(rawResult)).toEqual(['data']) expect(combine).toHaveBeenCalledTimes(1) @@ -584,8 +579,7 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ], - combine, - undefined, + { combine }, ) const initialCombined = getInitialCombined(initialRaw) @@ -594,8 +588,7 @@ describe('queriesObserver', () => { const newQueries = [{ queryKey: key1, queryFn: queryFn1 }] const [newRaw, getNewCombined] = observer.getOptimisticResult( newQueries, - combine, - undefined, + { combine }, ) const newCombined = getNewCombined(newRaw) @@ -622,8 +615,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, - undefined, + { combine }, ) const initialCombined = getInitialCombined(initialRaw) @@ -631,8 +623,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( [{ queryKey: key2, queryFn: queryFn2 }], - combine, - undefined, + { combine }, ) const newCombined = getNewCombined(newRaw) @@ -665,14 +656,12 @@ describe('queriesObserver', () => { const [raw1, getCombined1] = observer.getOptimisticResult( queries, - combine1, - undefined, + { combine: combine1 }, ) const combined1 = getCombined1(raw1) const [raw2, getCombined2] = observer.getOptimisticResult( queries, - combine2, - undefined, + { combine: combine2 }, ) const combined2 = getCombined2(raw2) @@ -701,8 +690,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, - false, + { combine, structuralSharing: false }, ) const initialCombined = getInitialCombined(initialRaw) @@ -714,8 +702,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine2, - false, + { combine: combine2, structuralSharing: false }, ) const newCombined = getNewCombined(newRaw) @@ -745,8 +732,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, - true, + { combine, structuralSharing: true }, ) const initialCombined = getInitialCombined(initialRaw) @@ -758,8 +744,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine2, - true, + { combine: combine2, structuralSharing: true }, ) const newCombined = getNewCombined(newRaw) @@ -798,8 +783,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, - customStructuralSharing, + { combine, structuralSharing: customStructuralSharing }, ) const initialCombined = getInitialCombined(initialRaw) @@ -836,8 +820,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, - customStructuralSharing, + { combine, structuralSharing: customStructuralSharing }, ) const initialCombined = getInitialCombined(initialRaw) @@ -849,8 +832,7 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, { queryKey: secondKey, queryFn: () => 2 }, ], - combine, - customStructuralSharing, + { combine, structuralSharing: customStructuralSharing }, ) const newCombined = getNewCombined(newRaw) @@ -884,8 +866,7 @@ describe('queriesObserver', () => { const [, getCombined] = observer.getOptimisticResult( [{ queryKey: key, queryFn }], - combine, - undefined, + { combine }, ) const combined = getCombined() @@ -905,7 +886,6 @@ describe('queriesObserver', () => { const [, , trackResult] = observer.getOptimisticResult( [{ queryKey: key, queryFn, notifyOnChangeProps: ['data'] }], undefined, - undefined, ) const trackedResults = trackResult() @@ -935,15 +915,13 @@ describe('queriesObserver', () => { const [raw1, getCombined1] = observer.getOptimisticResult( queries, - combine, - undefined, + { combine }, ) const combined1 = getCombined1(raw1) const [raw2, getCombined2] = observer.getOptimisticResult( queries, - combine, - undefined, + { combine }, ) const combined2 = getCombined2(raw2) @@ -972,15 +950,13 @@ describe('queriesObserver', () => { const [raw1, getCombined1] = observer.getOptimisticResult( queries, - combine, - undefined, + { combine }, ) getCombined1(raw1) const [raw2, getCombined2] = observer.getOptimisticResult( queries, - combine, - undefined, + { combine }, ) getCombined2(raw2) @@ -1007,7 +983,6 @@ describe('queriesObserver', () => { { queryKey: key2, queryFn: queryFn2 }, ], undefined, - undefined, ) const trackedResults = trackResult() @@ -1113,8 +1088,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, - customStructuralSharing, + { combine, structuralSharing: customStructuralSharing }, ) const initialCombined = getInitialCombined(initialRaw) const initialArrayRef = initialCombined.existingArray @@ -1128,8 +1102,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine2, - customStructuralSharing, + { combine: combine2, structuralSharing: customStructuralSharing }, ) const newCombined = getNewCombined(newRaw) diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index b5c3a65bdb1..48ce86f1081 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -180,11 +180,7 @@ export class QueriesObserver< getOptimisticResult( queries: Array, - combine: CombineFn | undefined, - structuralSharing: - | boolean - | ((oldData: unknown | undefined, newData: unknown) => unknown) - | undefined, + options?: QueriesObserverOptions, ): [ rawResult: Array, combineResult: (r?: Array) => TCombinedResult, @@ -201,12 +197,7 @@ export class QueriesObserver< return [ result, (r?: Array) => { - return this.#combineResult( - r ?? result, - combine, - structuralSharing, - queryHashes, - ) + return this.#combineResult(r ?? result, options, queryHashes) }, () => { return this.#trackResult(result, matches) @@ -238,13 +229,11 @@ export class QueriesObserver< #combineResult( input: Array, - combine: CombineFn | undefined, - structuralSharing: - | boolean - | ((oldData: unknown | undefined, newData: unknown) => unknown) - | undefined = true, + options?: QueriesObserverOptions, queryHashes?: Array, ): TCombinedResult { + const combine = options?.combine + const structuralSharing = options?.structuralSharing ?? true if (combine) { const lastHashes = this.#lastQueryHashes const queryHashesChanged = @@ -338,13 +327,10 @@ export class QueriesObserver< if (this.hasListeners()) { const shouldSkipCombine = this.#shouldSkipCombine() const previousResult = this.#combinedResult + const newTracked = this.#trackResult(this.#result, this.#observerMatches) const newResult = shouldSkipCombine ? previousResult - : this.#combineResult( - this.#trackResult(this.#result, this.#observerMatches), - this.#options?.combine, - this.#options?.structuralSharing, - ) + : this.#combineResult(newTracked, this.#options) if (shouldSkipCombine || previousResult !== newResult) { notifyManager.batch(() => { diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index db5123e8862..18d4911d8fd 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -274,8 +274,7 @@ export function useQueries< const [optimisticResult, getCombinedResult, trackResult] = observer.getOptimisticResult( defaultedQueries, - (options as QueriesObserverOptions).combine, - options.structuralSharing, + options as QueriesObserverOptions, ) const shouldSubscribe = !isRestoring && subscribed diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index de67859b22d..79240e73862 100644 --- a/packages/solid-query/src/useQueries.ts +++ b/packages/solid-query/src/useQueries.ts @@ -236,9 +236,7 @@ export function useQueries< const [state, setState] = createStore( observer.getOptimisticResult( defaultedQueries(), - (queriesOptions() as QueriesObserverOptions).combine, - (queriesOptions() as QueriesObserverOptions) - .structuralSharing, + queriesOptions() as QueriesObserverOptions, )[1](), ) @@ -249,10 +247,7 @@ export function useQueries< setState( observer.getOptimisticResult( defaultedQueries(), - (queriesOptions() as QueriesObserverOptions) - .combine, - (queriesOptions() as QueriesObserverOptions) - .structuralSharing, + queriesOptions() as QueriesObserverOptions, )[1](), ), ), diff --git a/packages/svelte-query/src/createQueries.svelte.ts b/packages/svelte-query/src/createQueries.svelte.ts index 66d7a91bbee..c440e72e4cf 100644 --- a/packages/svelte-query/src/createQueries.svelte.ts +++ b/packages/svelte-query/src/createQueries.svelte.ts @@ -237,8 +237,7 @@ export function createQueries< function createResult() { const [_, getCombinedResult, trackResult] = observer.getOptimisticResult( resolvedQueryOptions, - derivedCreateQueriesOptions.combine as QueriesObserverOptions['combine'], - derivedCreateQueriesOptions.structuralSharing, + derivedCreateQueriesOptions as QueriesObserverOptions, ) return getCombinedResult(trackResult()) } diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts index bdcb851ed7f..f9c755428f1 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -304,8 +304,7 @@ export function useQueries< const getOptimisticResult = () => { const [results, getCombinedResult] = observer.getOptimisticResult( defaultedQueries.value, - (options as QueriesObserverOptions).combine, - options.structuralSharing, + options as QueriesObserverOptions, ) return getCombinedResult( @@ -315,8 +314,7 @@ export function useQueries< refetch: async (...args: Array) => { const [{ [index]: query }] = observer.getOptimisticResult( defaultedQueries.value, - (options as QueriesObserverOptions).combine, - options.structuralSharing, + options as QueriesObserverOptions, ) return query!.refetch(...args) From ad4a1ef3db60a9461a74d7d5ba31924dede50375 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:08:04 +0000 Subject: [PATCH 9/9] ci: apply automated fixes --- .../src/__tests__/queriesObserver.test.tsx | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/packages/query-core/src/__tests__/queriesObserver.test.tsx b/packages/query-core/src/__tests__/queriesObserver.test.tsx index f8361d9bfa0..4e942c39296 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -466,10 +466,9 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ] - const [newRaw, getNewCombined] = observer.getOptimisticResult( - newQueries, - { combine }, - ) + const [newRaw, getNewCombined] = observer.getOptimisticResult(newQueries, { + combine, + }) const newCombined = getNewCombined(newRaw) expect(newCombined.count).toBe(2) @@ -586,10 +585,9 @@ describe('queriesObserver', () => { expect(initialCombined.count).toBe(2) const newQueries = [{ queryKey: key1, queryFn: queryFn1 }] - const [newRaw, getNewCombined] = observer.getOptimisticResult( - newQueries, - { combine }, - ) + const [newRaw, getNewCombined] = observer.getOptimisticResult(newQueries, { + combine, + }) const newCombined = getNewCombined(newRaw) expect(newCombined.count).toBe(1) @@ -654,15 +652,13 @@ describe('queriesObserver', () => { { combine: combine1 }, ) - const [raw1, getCombined1] = observer.getOptimisticResult( - queries, - { combine: combine1 }, - ) + const [raw1, getCombined1] = observer.getOptimisticResult(queries, { + combine: combine1, + }) const combined1 = getCombined1(raw1) - const [raw2, getCombined2] = observer.getOptimisticResult( - queries, - { combine: combine2 }, - ) + const [raw2, getCombined2] = observer.getOptimisticResult(queries, { + combine: combine2, + }) const combined2 = getCombined2(raw2) expect(combined1.total).toBe(2) @@ -913,16 +909,14 @@ describe('queriesObserver', () => { { combine }, ) - const [raw1, getCombined1] = observer.getOptimisticResult( - queries, - { combine }, - ) + const [raw1, getCombined1] = observer.getOptimisticResult(queries, { + combine, + }) const combined1 = getCombined1(raw1) - const [raw2, getCombined2] = observer.getOptimisticResult( - queries, - { combine }, - ) + const [raw2, getCombined2] = observer.getOptimisticResult(queries, { + combine, + }) const combined2 = getCombined2(raw2) // Same combine, same queries → cached result returned @@ -948,16 +942,14 @@ describe('queriesObserver', () => { combine, }) - const [raw1, getCombined1] = observer.getOptimisticResult( - queries, - { combine }, - ) + const [raw1, getCombined1] = observer.getOptimisticResult(queries, { + combine, + }) getCombined1(raw1) - const [raw2, getCombined2] = observer.getOptimisticResult( - queries, - { combine }, - ) + const [raw2, getCombined2] = observer.getOptimisticResult(queries, { + combine, + }) getCombined2(raw2) expect(combine).toHaveBeenCalledTimes(1)