diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md index b195e050d0..7aad8ff00a 100644 --- a/docs/src/pages/reference/QueryClient.md +++ b/docs/src/pages/reference/QueryClient.md @@ -474,6 +474,9 @@ The `getQueryDefaults` method returns the default options which have been set fo const defaultOptions = queryClient.getQueryDefaults(['posts']) ``` +> Note that if several query defaults match the given query key, the **first** matching one is returned. +> This could lead to unexpected behaviours. See [`setQueryDefaults`](#queryclientsetquerydefaults). + ## `queryClient.setQueryDefaults` `setQueryDefaults` can be used to set default options for specific queries: @@ -491,6 +494,9 @@ function Component() { - `queryKey: QueryKey`: [Query Keys](../guides/query-keys) - `options: QueryOptions` +> As stated in [`getQueryDefaults`](#queryclientgetquerydefaults), the order of registration of query defaults does matter. +> Since the **first** matching defaults are returned by `getQueryDefaults`, the registration should be made in the following order: from the **least generic key** to the **most generic one**. This way, in case of specific key, the first matching one would be the expected one. + ## `queryClient.getMutationDefaults` The `getMutationDefaults` method returns the default options which have been set for specific mutations: @@ -516,6 +522,8 @@ function Component() { - `mutationKey: string | unknown[]` - `options: MutationOptions` +> Similar to [`setQueryDefaults`](#queryclientsetquerydefaults), the order of registration does matter here. + ## `queryClient.getQueryCache` The `getQueryCache` method returns the query cache this client is connected to. diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts index 7e2fbc02ab..d09461ef83 100644 --- a/src/core/queryClient.ts +++ b/src/core/queryClient.ts @@ -38,6 +38,7 @@ import { onlineManager } from './onlineManager' import { notifyManager } from './notifyManager' import { infiniteQueryBehavior } from './infiniteQueryBehavior' import { CancelOptions, DefaultedQueryObserverOptions } from './types' +import { getLogger } from './logger' // TYPES @@ -535,10 +536,32 @@ export class QueryClient { getQueryDefaults( queryKey?: QueryKey ): QueryObserverOptions | undefined { - return queryKey - ? this.queryDefaults.find(x => partialMatchKey(queryKey, x.queryKey)) - ?.defaultOptions - : undefined + if (!queryKey) { + return undefined + } + + // Get the first matching defaults + const firstMatchingDefaults = this.queryDefaults.find(x => + partialMatchKey(queryKey, x.queryKey) + ) + + // Additional checks and error in dev mode + if (process.env.NODE_ENV !== 'production') { + // Retrieve all matching defaults for the given key + const matchingDefaults = this.queryDefaults.filter(x => + partialMatchKey(queryKey, x.queryKey) + ) + // It is ok not having defaults, but it is error prone to have more than 1 default for a given key + if (matchingDefaults.length > 1) { + getLogger().error( + `[QueryClient] Several query defaults match with key '${JSON.stringify( + queryKey + )}'. The first matching query defaults are used. Please check how query defaults are registered. Order does matter here. cf. https://react-query.tanstack.com/reference/QueryClient#queryclientsetquerydefaults.` + ) + } + } + + return firstMatchingDefaults?.defaultOptions } setMutationDefaults( @@ -558,11 +581,32 @@ export class QueryClient { getMutationDefaults( mutationKey?: MutationKey ): MutationObserverOptions | undefined { - return mutationKey - ? this.mutationDefaults.find(x => - partialMatchKey(mutationKey, x.mutationKey) - )?.defaultOptions - : undefined + if (!mutationKey) { + return undefined + } + + // Get the first matching defaults + const firstMatchingDefaults = this.mutationDefaults.find(x => + partialMatchKey(mutationKey, x.mutationKey) + ) + + // Additional checks and error in dev mode + if (process.env.NODE_ENV !== 'production') { + // Retrieve all matching defaults for the given key + const matchingDefaults = this.mutationDefaults.filter(x => + partialMatchKey(mutationKey, x.mutationKey) + ) + // It is ok not having defaults, but it is error prone to have more than 1 default for a given key + if (matchingDefaults.length > 1) { + getLogger().error( + `[QueryClient] Several mutation defaults match with key '${JSON.stringify( + mutationKey + )}'. The first matching mutation defaults are used. Please check how mutation defaults are registered. Order does matter here. cf. https://react-query.tanstack.com/reference/QueryClient#queryclientsetmutationdefaults.` + ) + } + } + + return firstMatchingDefaults?.defaultOptions } defaultQueryOptions< diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx index 0331415032..c32cf97cc0 100644 --- a/src/core/tests/queryClient.test.tsx +++ b/src/core/tests/queryClient.test.tsx @@ -130,6 +130,132 @@ describe('queryClient', () => { queryClient.setQueryDefaults(key, queryOptions2) expect(queryClient.getQueryDefaults(key)).toMatchObject(queryOptions2) }) + + test('should warn in dev if several query defaults match a given key', () => { + // Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199 + const consoleErrorMock = jest.spyOn(console, 'error') + consoleErrorMock.mockImplementation(() => true) + + const keyABCD = [ + { + a: 'a', + b: 'b', + c: 'c', + d: 'd', + }, + ] + + // The key below "contains" keyABCD => it is more generic + const keyABC = [ + { + a: 'a', + b: 'b', + c: 'c', + }, + ] + + // The defaults for query matching key "ABCD" (least generic) + const defaultsOfABCD = { + queryFn: function ABCDQueryFn() { + return 'ABCD' + }, + } + + // The defaults for query matching key "ABC" (most generic) + const defaultsOfABC = { + queryFn: function ABCQueryFn() { + return 'ABC' + }, + } + + // No defaults, no warning + const noDefaults = queryClient.getQueryDefaults(keyABCD) + expect(noDefaults).toBeUndefined() + expect(consoleErrorMock).not.toHaveBeenCalled() + + // If defaults for key ABCD are registered **before** the ones of key ABC (more generic)… + queryClient.setQueryDefaults(keyABCD, defaultsOfABCD) + queryClient.setQueryDefaults(keyABC, defaultsOfABC) + // … then the "good" defaults are retrieved: we get the ones for key "ABCD" + const goodDefaults = queryClient.getQueryDefaults(keyABCD) + expect(goodDefaults).toBe(defaultsOfABCD) + // The warning is still raised since several defaults are matching + expect(consoleErrorMock).toHaveBeenCalledTimes(1) + + // Let's create another queryClient and change the order of registration + const newQueryClient = new QueryClient() + // The defaults for key ABC (more generic) are registered **before** the ones of key ABCD… + newQueryClient.setQueryDefaults(keyABC, defaultsOfABC) + newQueryClient.setQueryDefaults(keyABCD, defaultsOfABCD) + // … then the "wrong" defaults are retrieved: we get the ones for key "ABC" + const badDefaults = newQueryClient.getQueryDefaults(keyABCD) + expect(badDefaults).not.toBe(defaultsOfABCD) + expect(badDefaults).toBe(defaultsOfABC) + expect(consoleErrorMock).toHaveBeenCalledTimes(2) + + consoleErrorMock.mockRestore() + }) + + test('should warn in dev if several mutation defaults match a given key', () => { + // Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199 + const consoleErrorMock = jest.spyOn(console, 'error') + consoleErrorMock.mockImplementation(() => true) + + const keyABCD = [ + { + a: 'a', + b: 'b', + c: 'c', + d: 'd', + }, + ] + + // The key below "contains" keyABCD => it is more generic + const keyABC = [ + { + a: 'a', + b: 'b', + c: 'c', + }, + ] + + // The defaults for mutation matching key "ABCD" (least generic) + const defaultsOfABCD = { + mutationFn: Promise.resolve, + } + + // The defaults for mutation matching key "ABC" (most generic) + const defaultsOfABC = { + mutationFn: Promise.resolve, + } + + // No defaults, no warning + const noDefaults = queryClient.getMutationDefaults(keyABCD) + expect(noDefaults).toBeUndefined() + expect(consoleErrorMock).not.toHaveBeenCalled() + + // If defaults for key ABCD are registered **before** the ones of key ABC (more generic)… + queryClient.setMutationDefaults(keyABCD, defaultsOfABCD) + queryClient.setMutationDefaults(keyABC, defaultsOfABC) + // … then the "good" defaults are retrieved: we get the ones for key "ABCD" + const goodDefaults = queryClient.getMutationDefaults(keyABCD) + expect(goodDefaults).toBe(defaultsOfABCD) + // The warning is still raised since several defaults are matching + expect(consoleErrorMock).toHaveBeenCalledTimes(1) + + // Let's create another queryClient and change the order of registration + const newQueryClient = new QueryClient() + // The defaults for key ABC (more generic) are registered **before** the ones of key ABCD… + newQueryClient.setMutationDefaults(keyABC, defaultsOfABC) + newQueryClient.setMutationDefaults(keyABCD, defaultsOfABCD) + // … then the "wrong" defaults are retrieved: we get the ones for key "ABC" + const badDefaults = newQueryClient.getMutationDefaults(keyABCD) + expect(badDefaults).not.toBe(defaultsOfABCD) + expect(badDefaults).toBe(defaultsOfABC) + expect(consoleErrorMock).toHaveBeenCalledTimes(2) + + consoleErrorMock.mockRestore() + }) }) describe('setQueryData', () => {