Skip to content

Commit e90e7f8

Browse files
GLabatGuillaume LabatTkDodo
authored
refactor(queryClient): add dev warning with queryDefaults (#3249)
* refactor(QueryClient): add dev warning Warn when several query defaults match a given key. Could be error prone if the returned defaults are not the expected ones. The order of registration does matter. * test(QueryClient): warning with defaults options Highlight how query defaults registration order matters. * doc(QueryClient): add notes about query defaults In `getQueryDefaults`, the **first** matching default is returned. In `setQueryDefaults`, highlight how the registration order is important. * doc(QueryClient): fix link to documentation * test(QueryClient): better test * refactor(QueryClient): use internal logger * doc(QueryClient): fix markup * doc(QueryClient): remove extra entry * refacto(QueryClient): warn about several query defaults Warning must be displayed any time a conflict is detected, not just for dev build. The warning is aimed at helping developers *using* react-query, not those *developping* react-query. * Update src/core/queryClient.ts Remove useless optional chaining. Co-authored-by: Dominik Dorfmeister <[email protected]> * feat(utils): add assert helper * refactor(QueryClient): add dev warning for mutation defaults * Revert "feat(utils): add assert helper" This reverts commit 05c3fe1. * refactor(QueryClient): error when several defaults Review how the check for multiple defaults on a key is raised. Ensure it remains fast in release build. * refactor(QueryClient): inline code Co-authored-by: Guillaume Labat <[email protected]> Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 52ad9cc commit e90e7f8

File tree

3 files changed

+187
-9
lines changed

3 files changed

+187
-9
lines changed

docs/src/pages/reference/QueryClient.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,9 @@ The `getQueryDefaults` method returns the default options which have been set fo
474474
const defaultOptions = queryClient.getQueryDefaults(['posts'])
475475
```
476476

477+
> Note that if several query defaults match the given query key, the **first** matching one is returned.
478+
> This could lead to unexpected behaviours. See [`setQueryDefaults`](#queryclientsetquerydefaults).
479+
477480
## `queryClient.setQueryDefaults`
478481

479482
`setQueryDefaults` can be used to set default options for specific queries:
@@ -491,6 +494,9 @@ function Component() {
491494
- `queryKey: QueryKey`: [Query Keys](../guides/query-keys)
492495
- `options: QueryOptions`
493496

497+
> As stated in [`getQueryDefaults`](#queryclientgetquerydefaults), the order of registration of query defaults does matter.
498+
> 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.
499+
494500
## `queryClient.getMutationDefaults`
495501

496502
The `getMutationDefaults` method returns the default options which have been set for specific mutations:
@@ -516,6 +522,8 @@ function Component() {
516522
- `mutationKey: string | unknown[]`
517523
- `options: MutationOptions`
518524

525+
> Similar to [`setQueryDefaults`](#queryclientsetquerydefaults), the order of registration does matter here.
526+
519527
## `queryClient.getQueryCache`
520528

521529
The `getQueryCache` method returns the query cache this client is connected to.

src/core/queryClient.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { onlineManager } from './onlineManager'
3838
import { notifyManager } from './notifyManager'
3939
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
4040
import { CancelOptions, DefaultedQueryObserverOptions } from './types'
41+
import { getLogger } from './logger'
4142

4243
// TYPES
4344

@@ -535,10 +536,32 @@ export class QueryClient {
535536
getQueryDefaults(
536537
queryKey?: QueryKey
537538
): QueryObserverOptions<any, any, any, any, any> | undefined {
538-
return queryKey
539-
? this.queryDefaults.find(x => partialMatchKey(queryKey, x.queryKey))
540-
?.defaultOptions
541-
: undefined
539+
if (!queryKey) {
540+
return undefined
541+
}
542+
543+
// Get the first matching defaults
544+
const firstMatchingDefaults = this.queryDefaults.find(x =>
545+
partialMatchKey(queryKey, x.queryKey)
546+
)
547+
548+
// Additional checks and error in dev mode
549+
if (process.env.NODE_ENV !== 'production') {
550+
// Retrieve all matching defaults for the given key
551+
const matchingDefaults = this.queryDefaults.filter(x =>
552+
partialMatchKey(queryKey, x.queryKey)
553+
)
554+
// It is ok not having defaults, but it is error prone to have more than 1 default for a given key
555+
if (matchingDefaults.length > 1) {
556+
getLogger().error(
557+
`[QueryClient] Several query defaults match with key '${JSON.stringify(
558+
queryKey
559+
)}'. 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.`
560+
)
561+
}
562+
}
563+
564+
return firstMatchingDefaults?.defaultOptions
542565
}
543566

544567
setMutationDefaults(
@@ -558,11 +581,32 @@ export class QueryClient {
558581
getMutationDefaults(
559582
mutationKey?: MutationKey
560583
): MutationObserverOptions<any, any, any, any> | undefined {
561-
return mutationKey
562-
? this.mutationDefaults.find(x =>
563-
partialMatchKey(mutationKey, x.mutationKey)
564-
)?.defaultOptions
565-
: undefined
584+
if (!mutationKey) {
585+
return undefined
586+
}
587+
588+
// Get the first matching defaults
589+
const firstMatchingDefaults = this.mutationDefaults.find(x =>
590+
partialMatchKey(mutationKey, x.mutationKey)
591+
)
592+
593+
// Additional checks and error in dev mode
594+
if (process.env.NODE_ENV !== 'production') {
595+
// Retrieve all matching defaults for the given key
596+
const matchingDefaults = this.mutationDefaults.filter(x =>
597+
partialMatchKey(mutationKey, x.mutationKey)
598+
)
599+
// It is ok not having defaults, but it is error prone to have more than 1 default for a given key
600+
if (matchingDefaults.length > 1) {
601+
getLogger().error(
602+
`[QueryClient] Several mutation defaults match with key '${JSON.stringify(
603+
mutationKey
604+
)}'. 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.`
605+
)
606+
}
607+
}
608+
609+
return firstMatchingDefaults?.defaultOptions
566610
}
567611

568612
defaultQueryOptions<

src/core/tests/queryClient.test.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,132 @@ describe('queryClient', () => {
130130
queryClient.setQueryDefaults(key, queryOptions2)
131131
expect(queryClient.getQueryDefaults(key)).toMatchObject(queryOptions2)
132132
})
133+
134+
test('should warn in dev if several query defaults match a given key', () => {
135+
// Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199
136+
const consoleErrorMock = jest.spyOn(console, 'error')
137+
consoleErrorMock.mockImplementation(() => true)
138+
139+
const keyABCD = [
140+
{
141+
a: 'a',
142+
b: 'b',
143+
c: 'c',
144+
d: 'd',
145+
},
146+
]
147+
148+
// The key below "contains" keyABCD => it is more generic
149+
const keyABC = [
150+
{
151+
a: 'a',
152+
b: 'b',
153+
c: 'c',
154+
},
155+
]
156+
157+
// The defaults for query matching key "ABCD" (least generic)
158+
const defaultsOfABCD = {
159+
queryFn: function ABCDQueryFn() {
160+
return 'ABCD'
161+
},
162+
}
163+
164+
// The defaults for query matching key "ABC" (most generic)
165+
const defaultsOfABC = {
166+
queryFn: function ABCQueryFn() {
167+
return 'ABC'
168+
},
169+
}
170+
171+
// No defaults, no warning
172+
const noDefaults = queryClient.getQueryDefaults(keyABCD)
173+
expect(noDefaults).toBeUndefined()
174+
expect(consoleErrorMock).not.toHaveBeenCalled()
175+
176+
// If defaults for key ABCD are registered **before** the ones of key ABC (more generic)…
177+
queryClient.setQueryDefaults(keyABCD, defaultsOfABCD)
178+
queryClient.setQueryDefaults(keyABC, defaultsOfABC)
179+
// … then the "good" defaults are retrieved: we get the ones for key "ABCD"
180+
const goodDefaults = queryClient.getQueryDefaults(keyABCD)
181+
expect(goodDefaults).toBe(defaultsOfABCD)
182+
// The warning is still raised since several defaults are matching
183+
expect(consoleErrorMock).toHaveBeenCalledTimes(1)
184+
185+
// Let's create another queryClient and change the order of registration
186+
const newQueryClient = new QueryClient()
187+
// The defaults for key ABC (more generic) are registered **before** the ones of key ABCD…
188+
newQueryClient.setQueryDefaults(keyABC, defaultsOfABC)
189+
newQueryClient.setQueryDefaults(keyABCD, defaultsOfABCD)
190+
// … then the "wrong" defaults are retrieved: we get the ones for key "ABC"
191+
const badDefaults = newQueryClient.getQueryDefaults(keyABCD)
192+
expect(badDefaults).not.toBe(defaultsOfABCD)
193+
expect(badDefaults).toBe(defaultsOfABC)
194+
expect(consoleErrorMock).toHaveBeenCalledTimes(2)
195+
196+
consoleErrorMock.mockRestore()
197+
})
198+
199+
test('should warn in dev if several mutation defaults match a given key', () => {
200+
// Check discussion here: https://github.com/tannerlinsley/react-query/discussions/3199
201+
const consoleErrorMock = jest.spyOn(console, 'error')
202+
consoleErrorMock.mockImplementation(() => true)
203+
204+
const keyABCD = [
205+
{
206+
a: 'a',
207+
b: 'b',
208+
c: 'c',
209+
d: 'd',
210+
},
211+
]
212+
213+
// The key below "contains" keyABCD => it is more generic
214+
const keyABC = [
215+
{
216+
a: 'a',
217+
b: 'b',
218+
c: 'c',
219+
},
220+
]
221+
222+
// The defaults for mutation matching key "ABCD" (least generic)
223+
const defaultsOfABCD = {
224+
mutationFn: Promise.resolve,
225+
}
226+
227+
// The defaults for mutation matching key "ABC" (most generic)
228+
const defaultsOfABC = {
229+
mutationFn: Promise.resolve,
230+
}
231+
232+
// No defaults, no warning
233+
const noDefaults = queryClient.getMutationDefaults(keyABCD)
234+
expect(noDefaults).toBeUndefined()
235+
expect(consoleErrorMock).not.toHaveBeenCalled()
236+
237+
// If defaults for key ABCD are registered **before** the ones of key ABC (more generic)…
238+
queryClient.setMutationDefaults(keyABCD, defaultsOfABCD)
239+
queryClient.setMutationDefaults(keyABC, defaultsOfABC)
240+
// … then the "good" defaults are retrieved: we get the ones for key "ABCD"
241+
const goodDefaults = queryClient.getMutationDefaults(keyABCD)
242+
expect(goodDefaults).toBe(defaultsOfABCD)
243+
// The warning is still raised since several defaults are matching
244+
expect(consoleErrorMock).toHaveBeenCalledTimes(1)
245+
246+
// Let's create another queryClient and change the order of registration
247+
const newQueryClient = new QueryClient()
248+
// The defaults for key ABC (more generic) are registered **before** the ones of key ABCD…
249+
newQueryClient.setMutationDefaults(keyABC, defaultsOfABC)
250+
newQueryClient.setMutationDefaults(keyABCD, defaultsOfABCD)
251+
// … then the "wrong" defaults are retrieved: we get the ones for key "ABC"
252+
const badDefaults = newQueryClient.getMutationDefaults(keyABCD)
253+
expect(badDefaults).not.toBe(defaultsOfABCD)
254+
expect(badDefaults).toBe(defaultsOfABC)
255+
expect(consoleErrorMock).toHaveBeenCalledTimes(2)
256+
257+
consoleErrorMock.mockRestore()
258+
})
133259
})
134260

135261
describe('setQueryData', () => {

0 commit comments

Comments
 (0)