From 05a61b102d4089e7f8be9933456580e3846dc6ce Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Wed, 4 Jan 2023 00:29:55 -0800 Subject: [PATCH] fix(query-core): allow multiple roots to share same QueryClient (#4636) * fix(query-core): allow multiple roots to share same QueryClient If two roots shared the `QueryClient` instance, then it would never unsubscribe from online/focus manager listeners and would also forever receive duplicate events because the unsubscriber functions would get "forgotten". These issues would be especially noticeable on React Native apps where multiple roots that share the same `QueryClient` are common. * test(QueryClient): Add test for multiple mounts, single unmount Co-authored-by: Dominik Dorfmeister --- packages/query-core/src/queryClient.ts | 11 +++ .../query-core/src/tests/queryClient.test.tsx | 70 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 62aebf93a6..15df263919 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -62,6 +62,7 @@ export class QueryClient { private defaultOptions: DefaultOptions private queryDefaults: QueryDefaults[] private mutationDefaults: MutationDefaults[] + private mountCount: number private unsubscribeFocus?: () => void private unsubscribeOnline?: () => void @@ -72,6 +73,7 @@ export class QueryClient { this.defaultOptions = config.defaultOptions || {} this.queryDefaults = [] this.mutationDefaults = [] + this.mountCount = 0 if (process.env.NODE_ENV !== 'production' && config.logger) { this.logger.error( @@ -81,6 +83,9 @@ export class QueryClient { } mount(): void { + this.mountCount++ + if (this.mountCount !== 1) return + this.unsubscribeFocus = focusManager.subscribe(() => { if (focusManager.isFocused()) { this.resumePausedMutations() @@ -96,8 +101,14 @@ export class QueryClient { } unmount(): void { + this.mountCount-- + if (this.mountCount !== 0) return + this.unsubscribeFocus?.() + this.unsubscribeFocus = undefined + this.unsubscribeOnline?.() + this.unsubscribeOnline = undefined } isFetching(filters?: QueryFilters): number diff --git a/packages/query-core/src/tests/queryClient.test.tsx b/packages/query-core/src/tests/queryClient.test.tsx index 8a91ef4632..a8b5533b09 100644 --- a/packages/query-core/src/tests/queryClient.test.tsx +++ b/packages/query-core/src/tests/queryClient.test.tsx @@ -23,6 +23,7 @@ describe('queryClient', () => { afterEach(() => { queryClient.clear() + queryClient.unmount() }) describe('defaultOptions', () => { @@ -1466,6 +1467,75 @@ describe('queryClient', () => { mutationCacheResumePausedMutationsSpy.mockRestore() onlineManager.setOnline(undefined) }) + + test('should notify queryCache and mutationCache after multiple mounts and single unmount', async () => { + const testClient = createQueryClient() + testClient.mount() + testClient.mount() + testClient.unmount() + + const queryCacheOnFocusSpy = jest.spyOn( + testClient.getQueryCache(), + 'onFocus', + ) + const queryCacheOnOnlineSpy = jest.spyOn( + testClient.getQueryCache(), + 'onOnline', + ) + const mutationCacheResumePausedMutationsSpy = jest.spyOn( + testClient.getMutationCache(), + 'resumePausedMutations', + ) + + onlineManager.setOnline(true) + expect(queryCacheOnOnlineSpy).toHaveBeenCalledTimes(1) + expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(1) + + focusManager.setFocused(true) + expect(queryCacheOnFocusSpy).toHaveBeenCalledTimes(1) + expect(mutationCacheResumePausedMutationsSpy).toHaveBeenCalledTimes(2) + + queryCacheOnFocusSpy.mockRestore() + queryCacheOnOnlineSpy.mockRestore() + mutationCacheResumePausedMutationsSpy.mockRestore() + focusManager.setFocused(undefined) + onlineManager.setOnline(undefined) + }) + + test('should not notify queryCache and mutationCache after multiple mounts/unmounts', async () => { + const testClient = createQueryClient() + testClient.mount() + testClient.mount() + testClient.unmount() + testClient.unmount() + + const queryCacheOnFocusSpy = jest.spyOn( + testClient.getQueryCache(), + 'onFocus', + ) + const queryCacheOnOnlineSpy = jest.spyOn( + testClient.getQueryCache(), + 'onOnline', + ) + const mutationCacheResumePausedMutationsSpy = jest.spyOn( + testClient.getMutationCache(), + 'resumePausedMutations', + ) + + onlineManager.setOnline(true) + expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled() + expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled() + + focusManager.setFocused(true) + expect(queryCacheOnFocusSpy).not.toHaveBeenCalled() + expect(mutationCacheResumePausedMutationsSpy).not.toHaveBeenCalled() + + queryCacheOnFocusSpy.mockRestore() + queryCacheOnOnlineSpy.mockRestore() + mutationCacheResumePausedMutationsSpy.mockRestore() + focusManager.setFocused(undefined) + onlineManager.setOnline(undefined) + }) }) describe('setMutationDefaults', () => {