Repository: tannerlinsley/react-query
Pull request #3249 (2022-01-31 11:50:06)

doc(queryClient): add dev warning with queryDefaults
Whenever several queryDefaults match a queryKey, a dev warning message is raised since this is error-prone.
Test added to highlight the behaviour.

Related to https://github.com/tannerlinsley/react-query/discussions/3199.

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tanstack/react-query/6JM9k6rtzHdyhkXymxM9ceu4sNrT](https://vercel.com/tanstack/react-query/6JM9k6rtzHdyhkXymxM9ceu4sNrT)  
✅ Preview: *In Progress*


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/tannerlinsley/react-query/pr/3249/builds/213551) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 137466bb6bf7a710fed65efff792d9df20025468:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query: basic](https://codesandbox.io/s/tannerlinsley-react-query-basic-29y5o)| Configuration |
|[tannerlinsley/react-query: basic-typescript](https://codesandbox.io/s/tannerlinsley-react-query-basic-typescript-sfrwi)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3249?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) Report
> Merging [#3249](https://codecov.io/gh/tannerlinsley/react-query/pull/3249?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (78501ee) into [alpha](https://codecov.io/gh/tannerlinsley/react-query/commit/c0fc916cb3e7d32a05e9356b2f1359c3cd5de573?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) (c0fc916) will **decrease** coverage by `18.17%`.
> The diff coverage is `70.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/graphs/tree.svg?width=650&height=150&src=pr&token=jqEbswkEDQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)](https://codecov.io/gh/tannerlinsley/react-query/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)

```diff
@@             Coverage Diff             @@
##            alpha    #3249       +/-   ##
===========================================
- Coverage   96.82%   78.64%   -18.18%     
===========================================
  Files          46       46               
  Lines        2302     1658      -644     
  Branches      668      337      -331     
===========================================
- Hits         2229     1304      -925     
+ Misses         70       40       -30     
- Partials        3      314      +311     
```


| [Impacted Files](https://codecov.io/gh/tannerlinsley/react-query/pull/3249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) | Coverage Δ | |
|---|---|---|
| [src/core/queryClient.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnlDbGllbnQudHM=) | `83.05% <70.00%> (-16.95%)` | :arrow_down: |
| [src/core/utils.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvdXRpbHMudHM=) | `57.89% <0.00%> (-42.11%)` | :arrow_down: |
| [src/core/hydration.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvaHlkcmF0aW9uLnRz) | `63.63% <0.00%> (-36.37%)` | :arrow_down: |
| [src/reactjs/utils.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL3JlYWN0anMvdXRpbHMudHM=) | `66.66% <0.00%> (-33.34%)` | :arrow_down: |
| [src/core/infiniteQueryBehavior.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvaW5maW5pdGVRdWVyeUJlaGF2aW9yLnRz) | `68.85% <0.00%> (-31.15%)` | :arrow_down: |
| [src/core/retryer.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcmV0cnllci50cw==) | `76.05% <0.00%> (-23.95%)` | :arrow_down: |
| [src/core/query.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnkudHM=) | `76.25% <0.00%> (-23.75%)` | :arrow_down: |
| [src/core/queryObserver.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvcXVlcnlPYnNlcnZlci50cw==) | `76.95% <0.00%> (-23.05%)` | :arrow_down: |
| [src/core/focusManager.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvZm9jdXNNYW5hZ2VyLnRz) | `77.41% <0.00%> (-22.59%)` | :arrow_down: |
| [src/core/onlineManager.ts](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#diff-c3JjL2NvcmUvb25saW5lTWFuYWdlci50cw==) | `77.41% <0.00%> (-22.59%)` | :arrow_down: |
| ... and [32 more](https://codecov.io/gh/tannerlinsley/react-query/pull/3249/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3249?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/3249?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Last update [c0fc916...78501ee](https://codecov.io/gh/tannerlinsley/react-query/pull/3249?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).

GLabat: > please update the branch with the latest `alpha` so that we can do a size comparison between the latest build and this branch.

Yup thanks.
TkDodo: awesome, size is now the same as on `alpha`. Thank you 🙇 

Patch:
diff --git a/docs/src/pages/reference/QueryClient.md b/docs/src/pages/reference/QueryClient.md
index b195e050d..7aad8ff00 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 7e2fbc02a..d09461ef8 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<any, any, any, any, any> | 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<any, any, any, any> | 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<


Test patch:
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index 033141503..c32cf97cc 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', () => {