diff --git a/.changeset/silver-coins-mix.md b/.changeset/silver-coins-mix.md new file mode 100644 index 00000000000..efc41ecea9f --- /dev/null +++ b/.changeset/silver-coins-mix.md @@ -0,0 +1,11 @@ +--- +'@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 +--- + +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..644536658b0 100644 --- a/docs/framework/react/reference/useQueries.md +++ b/docs/framework/react/reference/useQueries.md @@ -24,6 +24,12 @@ 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 | ((oldData: unknown | undefined, newData: unknown) => unknown)` + - Optional + - Defaults to `true`. + - 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. @@ -37,7 +43,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`, 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 d61a937a3c2..25c1923c6ef 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -211,6 +211,15 @@ export interface InjectQueriesOptions< ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries }, ] 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 + | ((oldData: unknown | undefined, newData: unknown) => unknown) } /** @@ -270,7 +279,7 @@ export function injectQueries< const optimisticResultSignal = computed(() => observerSignal().getOptimisticResult( defaultedQueries(), - (optionsSignal() as QueriesObserverOptions).combine, + optionsSignal() as QueriesObserverOptions, ), ) diff --git a/packages/preact-query/src/useQueries.ts b/packages/preact-query/src/useQueries.ts index dac252d0109..54ab373d8a5 100644 --- a/packages/preact-query/src/useQueries.ts +++ b/packages/preact-query/src/useQueries.ts @@ -216,6 +216,15 @@ 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. + * 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 + | ((oldData: unknown | undefined, newData: unknown) => unknown) subscribed?: boolean }, queryClient?: QueryClient, @@ -262,7 +271,7 @@ export function useQueries< const [optimisticResult, getCombinedResult, trackResult] = observer.getOptimisticResult( defaultedQueries, - (options as QueriesObserverOptions).combine, + 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 850ffa77146..4e942c39296 100644 --- a/packages/query-core/src/__tests__/queriesObserver.test.tsx +++ b/packages/query-core/src/__tests__/queriesObserver.test.tsx @@ -456,7 +456,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, + { combine }, ) const initialCombined = getInitialCombined(initialRaw) @@ -466,10 +466,9 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ] - const [newRaw, getNewCombined] = observer.getOptimisticResult( - newQueries, + const [newRaw, getNewCombined] = observer.getOptimisticResult(newQueries, { combine, - ) + }) const newCombined = getNewCombined(newRaw) expect(newCombined.count).toBe(2) @@ -495,7 +494,7 @@ describe('queriesObserver', () => { const [rawResult, getCombinedResult] = observer.getOptimisticResult( [query], - combine, + { combine }, ) expect(getCombinedResult(rawResult)).toEqual(['data']) expect(combine).toHaveBeenCalledTimes(1) @@ -528,7 +527,7 @@ describe('queriesObserver', () => { const [rawResult, getCombinedResult] = observer.getOptimisticResult( [query], - combine, + { combine }, ) expect(getCombinedResult(rawResult)).toEqual(['data']) expect(combine).toHaveBeenCalledTimes(1) @@ -579,17 +578,16 @@ describe('queriesObserver', () => { { queryKey: key1, queryFn: queryFn1 }, { queryKey: key2, queryFn: queryFn2 }, ], - combine, + { combine }, ) const initialCombined = getInitialCombined(initialRaw) expect(initialCombined.count).toBe(2) const newQueries = [{ queryKey: key1, queryFn: queryFn1 }] - const [newRaw, getNewCombined] = observer.getOptimisticResult( - newQueries, + const [newRaw, getNewCombined] = observer.getOptimisticResult(newQueries, { combine, - ) + }) const newCombined = getNewCombined(newRaw) expect(newCombined.count).toBe(1) @@ -615,7 +613,7 @@ describe('queriesObserver', () => { const [initialRaw, getInitialCombined] = observer.getOptimisticResult( [{ queryKey: key1, queryFn: queryFn1 }], - combine, + { combine }, ) const initialCombined = getInitialCombined(initialRaw) @@ -623,7 +621,7 @@ describe('queriesObserver', () => { const [newRaw, getNewCombined] = observer.getOptimisticResult( [{ queryKey: key2, queryFn: queryFn2 }], - combine, + { combine }, ) const newCombined = getNewCombined(newRaw) @@ -654,16 +652,200 @@ describe('queriesObserver', () => { { combine: combine1 }, ) - const [raw1, getCombined1] = observer.getOptimisticResult(queries, combine1) + const [raw1, getCombined1] = observer.getOptimisticResult(queries, { + combine: combine1, + }) const combined1 = getCombined1(raw1) - - const [raw2, getCombined2] = observer.getOptimisticResult(queries, combine2) + const [raw2, getCombined2] = observer.getOptimisticResult(queries, { + combine: combine2, + }) 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, structuralSharing: 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 }], + { combine: combine2, structuralSharing: 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, structuralSharing: 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 }], + { combine: combine2, structuralSharing: 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, structuralSharing: 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, structuralSharing: 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, structuralSharing: 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, @@ -680,7 +862,7 @@ describe('queriesObserver', () => { const [, getCombined] = observer.getOptimisticResult( [{ queryKey: key, queryFn }], - combine, + { combine }, ) const combined = getCombined() @@ -727,10 +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 @@ -756,16 +942,14 @@ describe('queriesObserver', () => { combine, }) - const [raw1, getCombined1] = observer.getOptimisticResult( - queries, + const [raw1, getCombined1] = observer.getOptimisticResult(queries, { combine, - ) + }) getCombined1(raw1) - const [raw2, getCombined2] = observer.getOptimisticResult( - queries, + const [raw2, getCombined2] = observer.getOptimisticResult(queries, { combine, - ) + }) getCombined2(raw2) expect(combine).toHaveBeenCalledTimes(1) @@ -851,4 +1035,70 @@ 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, structuralSharing: 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 }], + { combine: combine2, structuralSharing: customStructuralSharing }, + ) + const newCombined = getNewCombined(newRaw) + + // The existingArray reference should be retained from the old data + expect(newCombined.existingArray).toBe(initialArrayRef) + }) }) diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index 0f0cc7a440b..48ce86f1081 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, @@ -30,6 +30,15 @@ export interface QueriesObserverOptions< TCombinedResult = Array, > { 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 + | ((oldData: unknown | undefined, newData: unknown) => unknown) } export class QueriesObserver< @@ -171,7 +180,7 @@ export class QueriesObserver< getOptimisticResult( queries: Array, - combine: CombineFn | undefined, + options?: QueriesObserverOptions, ): [ rawResult: Array, combineResult: (r?: Array) => TCombinedResult, @@ -188,7 +197,7 @@ export class QueriesObserver< return [ result, (r?: Array) => { - return this.#combineResult(r ?? result, combine, queryHashes) + return this.#combineResult(r ?? result, options, queryHashes) }, () => { return this.#trackResult(result, matches) @@ -220,9 +229,11 @@ export class QueriesObserver< #combineResult( input: Array, - combine: CombineFn | undefined, + options?: QueriesObserverOptions, queryHashes?: Array, ): TCombinedResult { + const combine = options?.combine + const structuralSharing = options?.structuralSharing ?? true if (combine) { const lastHashes = this.#lastQueryHashes const queryHashesChanged = @@ -242,9 +253,14 @@ export class QueriesObserver< if (queryHashes !== undefined) { this.#lastQueryHashes = queryHashes } - this.#combinedResult = replaceEqualDeep( + + this.#combinedResult = replaceData( this.#combinedResult, combine(input), + { + structuralSharing, + queryHash: queryHashes, + }, ) } @@ -311,12 +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.#combineResult(newTracked, this.#options) if (shouldSkipCombine || previousResult !== newResult) { notifyManager.batch(() => { 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. diff --git a/packages/react-query/src/useQueries.ts b/packages/react-query/src/useQueries.ts index 437718a988f..18d4911d8fd 100644 --- a/packages/react-query/src/useQueries.ts +++ b/packages/react-query/src/useQueries.ts @@ -216,6 +216,15 @@ 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. + * 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 + | ((oldData: unknown | undefined, newData: unknown) => unknown) subscribed?: boolean }, queryClient?: QueryClient, @@ -265,7 +274,7 @@ export function useQueries< const [optimisticResult, getCombinedResult, trackResult] = observer.getOptimisticResult( defaultedQueries, - (options as QueriesObserverOptions).combine, + options as QueriesObserverOptions, ) const shouldSubscribe = !isRestoring && subscribed diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index 9bf909634b2..79240e73862 100644 --- a/packages/solid-query/src/useQueries.ts +++ b/packages/solid-query/src/useQueries.ts @@ -193,6 +193,15 @@ 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. + * 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 + | ((oldData: unknown | undefined, newData: unknown) => unknown) }>, queryClient?: Accessor, ): TCombinedResult { @@ -219,6 +228,7 @@ export function useQueries< queriesOptions().combine ? ({ combine: queriesOptions().combine, + structuralSharing: queriesOptions().structuralSharing, } as QueriesObserverOptions) : undefined, ) @@ -226,7 +236,7 @@ export function useQueries< const [state, setState] = createStore( observer.getOptimisticResult( defaultedQueries(), - (queriesOptions() as QueriesObserverOptions).combine, + queriesOptions() as QueriesObserverOptions, )[1](), ) @@ -237,8 +247,7 @@ export function useQueries< setState( observer.getOptimisticResult( defaultedQueries(), - (queriesOptions() as QueriesObserverOptions) - .combine, + queriesOptions() as QueriesObserverOptions, )[1](), ), ), @@ -304,22 +313,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..c440e72e4cf 100644 --- a/packages/svelte-query/src/createQueries.svelte.ts +++ b/packages/svelte-query/src/createQueries.svelte.ts @@ -197,13 +197,23 @@ export function createQueries< ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries }, ] 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 + | ((oldData: unknown | undefined, newData: unknown) => unknown) }>, 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 +230,14 @@ 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 as QueriesObserverOptions, ) return getCombinedResult(trackResult()) } @@ -244,9 +254,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..f9c755428f1 100644 --- a/packages/vue-query/src/useQueries.ts +++ b/packages/vue-query/src/useQueries.ts @@ -248,6 +248,15 @@ 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 + | ((oldData: unknown | undefined, newData: unknown) => unknown) }, queryClient?: QueryClient, ): Readonly> { @@ -295,7 +304,7 @@ export function useQueries< const getOptimisticResult = () => { const [results, getCombinedResult] = observer.getOptimisticResult( defaultedQueries.value, - (options as QueriesObserverOptions).combine, + options as QueriesObserverOptions, ) return getCombinedResult( @@ -305,7 +314,7 @@ export function useQueries< refetch: async (...args: Array) => { const [{ [index]: query }] = observer.getOptimisticResult( defaultedQueries.value, - (options as QueriesObserverOptions).combine, + options as QueriesObserverOptions, ) return query!.refetch(...args)