From cae25241b9c9fbc9dfd91a89c0cf06ec9987fcd7 Mon Sep 17 00:00:00 2001 From: Dominik Dorfmeister Date: Wed, 9 Oct 2024 14:57:36 +0200 Subject: [PATCH] fix(core): do not inform QueriesObserver subscribers if combined result hasn't changed (#8153) --- packages/query-core/src/queriesObserver.ts | 27 ++-- .../src/__tests__/useQueries.test.tsx | 117 ++++++++++++++++++ 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts index 2c58e9221c..37557a6e72 100644 --- a/packages/query-core/src/queriesObserver.ts +++ b/packages/query-core/src/queriesObserver.ts @@ -38,6 +38,7 @@ export class QueriesObserver< #client: QueryClient #result!: Array #queries: Array + #options?: QueriesObserverOptions #observers: Array #combinedResult?: TCombinedResult #lastCombine?: CombineFn @@ -46,11 +47,12 @@ export class QueriesObserver< constructor( client: QueryClient, queries: Array>, - _options?: QueriesObserverOptions, + options?: QueriesObserverOptions, ) { super() this.#client = client + this.#options = options this.#queries = [] this.#observers = [] this.#result = [] @@ -83,10 +85,11 @@ export class QueriesObserver< setQueries( queries: Array, - _options?: QueriesObserverOptions, + options?: QueriesObserverOptions, notifyOptions?: NotifyOptions, ): void { this.#queries = queries + this.#options = options notifyManager.batch(() => { const prevObservers = this.#observers @@ -268,11 +271,21 @@ export class QueriesObserver< } #notify(): void { - notifyManager.batch(() => { - this.listeners.forEach((listener) => { - listener(this.#result) - }) - }) + if (this.hasListeners()) { + const previousResult = this.#combinedResult + const newResult = this.#combineResult( + this.#result, + this.#options?.combine, + ) + + if (previousResult !== newResult) { + notifyManager.batch(() => { + this.listeners.forEach((listener) => { + listener(this.#result) + }) + }) + } + } } } diff --git a/packages/react-query/src/__tests__/useQueries.test.tsx b/packages/react-query/src/__tests__/useQueries.test.tsx index fe520e6ece..788864e8c6 100644 --- a/packages/react-query/src/__tests__/useQueries.test.tsx +++ b/packages/react-query/src/__tests__/useQueries.test.tsx @@ -1431,4 +1431,121 @@ describe('useQueries', () => { // state changed, re-run combine expect(spy).toHaveBeenCalledTimes(4) }) + + it('should not re-render if combine returns a stable reference', async () => { + const key1 = queryKey() + const key2 = queryKey() + + const client = new QueryClient() + + const queryFns: Array = [] + let renders = 0 + + function Page() { + const data = useQueries( + { + queries: [ + { + queryKey: [key1], + queryFn: async () => { + await sleep(10) + queryFns.push('first result') + return 'first result' + }, + }, + { + queryKey: [key2], + queryFn: async () => { + await sleep(20) + queryFns.push('second result') + return 'second result' + }, + }, + ], + combine: () => 'foo', + }, + client, + ) + + renders++ + + return ( +
+
data: {data}
+
+ ) + } + + const rendered = render() + + await waitFor(() => rendered.getByText('data: foo')) + + await waitFor(() => + expect(queryFns).toEqual(['first result', 'second result']), + ) + + expect(renders).toBe(1) + }) + + it('should re-render once combine returns a different reference', async () => { + const key1 = queryKey() + const key2 = queryKey() + const key3 = queryKey() + + const client = new QueryClient() + + let renders = 0 + + function Page() { + const data = useQueries( + { + queries: [ + { + queryKey: [key1], + queryFn: async () => { + await sleep(10) + return 'first result' + }, + }, + { + queryKey: [key2], + queryFn: async () => { + await sleep(15) + return 'second result' + }, + }, + { + queryKey: [key3], + queryFn: async () => { + await sleep(20) + return 'third result' + }, + }, + ], + combine: (results) => { + const isPending = results.some((res) => res.isPending) + + return isPending ? 'pending' : 'foo' + }, + }, + client, + ) + + renders++ + + return ( +
+
data: {data}
+
+ ) + } + + const rendered = render() + + await waitFor(() => rendered.getByText('data: pending')) + await waitFor(() => rendered.getByText('data: foo')) + + // one with pending, one with foo + expect(renders).toBe(2) + }) })