diff --git a/.changeset/silver-coins-mix.md b/.changeset/silver-coins-mix.md new file mode 100644 index 0000000000..d02adb65ee --- /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 d41a73a2b1..1de3f37b77 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 2f201799e7..f96c104d48 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 24b80aaf81..3e8c46119d 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -300,6 +300,7 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, ], undefined, + undefined, )[0], ) @@ -414,6 +415,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], combine, + undefined, ) const initialCombined = getInitialCombined(initialRaw) @@ -426,6 +428,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( newQueries, combine, + undefined, ) const newCombined = getNewCombined(newRaw) @@ -461,6 +464,7 @@ describe('queriesObserver', () => { { queryKey: key2, queryFn: queryFn2 }, ], combine, + undefined, ) const initialCombined = getInitialCombined(initialRaw) @@ -470,6 +474,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( newQueries, combine, + undefined, ) const newCombined = getNewCombined(newRaw) @@ -497,6 +502,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], combine, + undefined, ) const initialCombined = getInitialCombined(initialRaw) @@ -505,6 +511,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 67dd088f9a..99fec07333 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) @@ -216,6 +228,7 @@ export class QueriesObserver< #combineResult( input: Array, combine: CombineFn | undefined, + structuralSharing: boolean | undefined = true, queryHashes?: Array, ): TCombinedResult { if (combine) { @@ -238,10 +251,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 @@ -296,7 +311,11 @@ export class QueriesObserver< if (this.hasListeners()) { const previousResult = this.#combinedResult const newTracked = this.#trackResult(this.#result, this.#observerMatches) - const newResult = this.#combineResult(newTracked, this.#options?.combine) + const newResult = this.#combineResult( + newTracked, + this.#options?.combine, + this.#options?.structuralSharing, + ) if (previousResult !== newResult) { notifyManager.batch(() => { diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 6eabef4060..3d8b02f23e 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -217,6 +217,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, @@ -264,6 +270,7 @@ export function useQueries< observer.getOptimisticResult( defaultedQueries, (options as QueriesObserverOptions).combine, + options.structuralSharing, ) const shouldSubscribe = !isRestoring && options.subscribed !== false diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index 1e5592775d..84800c39ad 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 { @@ -218,6 +224,7 @@ export function useQueries< queriesOptions().combine ? ({ combine: queriesOptions().combine, + structuralSharing: queriesOptions().structuralSharing, } as QueriesObserverOptions) : undefined, ) @@ -226,6 +233,8 @@ export function useQueries< observer.getOptimisticResult( defaultedQueries(), (queriesOptions() as QueriesObserverOptions).combine, + (queriesOptions() as QueriesObserverOptions) + .structuralSharing, )[1](), ) @@ -238,6 +247,8 @@ export function useQueries< defaultedQueries(), (queriesOptions() as QueriesObserverOptions) .combine, + (queriesOptions() as QueriesObserverOptions) + .structuralSharing, )[1](), ), ), @@ -303,22 +314,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 dec5756129..da266089d3 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 f290976a14..932c91e02b 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)