Skip to content

Commit

Permalink
feat: merge multiple query / mutation defaults (#4849)
Browse files Browse the repository at this point in the history
* refactor(core): query and mutationDefaults are now stored as a Map

* feat(core): query and mutation defaults can now be merged together

* chore: fix prettier

* Apply suggestions from code review

Co-authored-by: ecyrbe <ecyrbe@gmail.com>

* chore: fix types in vue

* refactor: don't use reduce

* chore: prettier

Co-authored-by: ecyrbe <ecyrbe@gmail.com>
  • Loading branch information
TkDodo and ecyrbe committed Jan 22, 2023
1 parent 8c373d2 commit 95ae966
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 193 deletions.
112 changes: 38 additions & 74 deletions packages/query-core/src/queryClient.ts
Expand Up @@ -56,8 +56,8 @@ export class QueryClient {
#mutationCache: MutationCache
#logger: Logger
#defaultOptions: DefaultOptions
#queryDefaults: QueryDefaults[]
#mutationDefaults: MutationDefaults[]
#queryDefaults: Map<string, QueryDefaults>
#mutationDefaults: Map<string, MutationDefaults>
#mountCount: number
#unsubscribeFocus?: () => void
#unsubscribeOnline?: () => void
Expand All @@ -67,8 +67,8 @@ export class QueryClient {
this.#mutationCache = config.mutationCache || new MutationCache()
this.#logger = config.logger || defaultLogger
this.#defaultOptions = config.defaultOptions || {}
this.#queryDefaults = []
this.#mutationDefaults = []
this.#queryDefaults = new Map()
this.#mutationDefaults = new Map()
this.#mountCount = 0

if (process.env.NODE_ENV !== 'production' && config.logger) {
Expand Down Expand Up @@ -366,92 +366,55 @@ export class QueryClient {

setQueryDefaults(
queryKey: QueryKey,
options: Omit<QueryObserverOptions<unknown, any, any, any>, 'queryKey'>,
options: Partial<
Omit<QueryObserverOptions<unknown, any, any, any>, 'queryKey'>
>,
): void {
const result = this.#queryDefaults.find(
(x) => hashKey(queryKey) === hashKey(x.queryKey),
)
if (result) {
result.defaultOptions = options
} else {
this.#queryDefaults.push({ queryKey, defaultOptions: options })
}
this.#queryDefaults.set(hashKey(queryKey), {
queryKey,
defaultOptions: options,
})
}

getQueryDefaults(
queryKey?: QueryKey,
): QueryObserverOptions<any, any, any, any, any> | undefined {
if (!queryKey) {
return undefined
}
queryKey: QueryKey,
): QueryObserverOptions<any, any, any, any, any> {
const defaults = [...this.#queryDefaults.values()]

// Get the first matching defaults
const firstMatchingDefaults = this.#queryDefaults.find((x) =>
partialMatchKey(queryKey, x.queryKey),
)
let result: QueryObserverOptions<any, any, any, any, any> = {}

// 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) {
this.#logger.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.`,
)
defaults.forEach((queryDefault) => {
if (partialMatchKey(queryKey, queryDefault.queryKey)) {
result = { ...result, ...queryDefault.defaultOptions }
}
}

return firstMatchingDefaults?.defaultOptions
})
return result
}

setMutationDefaults(
mutationKey: MutationKey,
options: MutationObserverOptions<any, any, any, any>,
options: Omit<MutationObserverOptions<any, any, any, any>, 'mutationKey'>,
): void {
const result = this.#mutationDefaults.find(
(x) => hashKey(mutationKey) === hashKey(x.mutationKey),
)
if (result) {
result.defaultOptions = options
} else {
this.#mutationDefaults.push({ mutationKey, defaultOptions: options })
}
this.#mutationDefaults.set(hashKey(mutationKey), {
mutationKey,
defaultOptions: options,
})
}

getMutationDefaults(
mutationKey?: MutationKey,
): MutationObserverOptions<any, any, any, any> | undefined {
if (!mutationKey) {
return undefined
}
mutationKey: MutationKey,
): MutationObserverOptions<any, any, any, any> {
const defaults = [...this.#mutationDefaults.values()]

// Get the first matching defaults
const firstMatchingDefaults = this.#mutationDefaults.find((x) =>
partialMatchKey(mutationKey, x.mutationKey),
)
let result: MutationObserverOptions<any, any, any, any> = {}

// 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) {
this.#logger.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.`,
)
defaults.forEach((queryDefault) => {
if (partialMatchKey(mutationKey, queryDefault.mutationKey)) {
result = { ...result, ...queryDefault.defaultOptions }
}
}
})

return firstMatchingDefaults?.defaultOptions
return result
}

defaultQueryOptions<
Expand Down Expand Up @@ -489,12 +452,12 @@ export class QueryClient {

const defaultedOptions = {
...this.#defaultOptions.queries,
...this.getQueryDefaults(options?.queryKey),
...(options?.queryKey && this.getQueryDefaults(options.queryKey)),
...options,
_defaulted: true,
}

if (!defaultedOptions.queryHash && defaultedOptions.queryKey) {
if (!defaultedOptions.queryHash) {
defaultedOptions.queryHash = hashQueryKeyByOptions(
defaultedOptions.queryKey,
defaultedOptions,
Expand Down Expand Up @@ -527,7 +490,8 @@ export class QueryClient {
}
return {
...this.#defaultOptions.mutations,
...this.getMutationDefaults(options?.mutationKey),
...(options?.mutationKey &&
this.getMutationDefaults(options.mutationKey)),
...options,
_defaulted: true,
} as T
Expand Down
124 changes: 9 additions & 115 deletions packages/query-core/src/tests/queryClient.test.tsx
Expand Up @@ -126,123 +126,17 @@ describe('queryClient', () => {
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 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(mockLogger.error).toHaveBeenCalledTimes(1)

// 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(mockLogger.error).toHaveBeenCalledTimes(2)

// Let's create another queryClient and change the order of registration
const newQueryClient = createQueryClient()
// 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(mockLogger.error).toHaveBeenCalledTimes(4)
})

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 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,
}
test('should merge defaultOptions', async () => {
const key = queryKey()

// No defaults, no warning
const noDefaults = queryClient.getMutationDefaults(keyABCD)
expect(noDefaults).toBeUndefined()
expect(mockLogger.error).toHaveBeenNthCalledWith(
1,
'Passing a custom logger has been deprecated and will be removed in the next major version.',
)
queryClient.setQueryDefaults([...key, 'todo'], { suspense: true })
queryClient.setQueryDefaults([...key, 'todo', 'detail'], {
staleTime: 5000,
})

// 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(mockLogger.error).toHaveBeenCalledTimes(2)

// Let's create another queryClient and change the order of registration
const newQueryClient = createQueryClient()
// 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(mockLogger.error).toHaveBeenCalledTimes(4)
expect(
queryClient.getQueryDefaults([...key, 'todo', 'detail']),
).toMatchObject({ suspense: true, staleTime: 5000 })
})
})

Expand Down
8 changes: 4 additions & 4 deletions packages/vue-query/src/queryClient.ts
Expand Up @@ -264,8 +264,8 @@ export class QueryClient extends QC {
}

getQueryDefaults(
queryKey?: MaybeRefDeep<QueryKey>,
): QueryObserverOptions<any, any, any, any, any> | undefined {
queryKey: MaybeRefDeep<QueryKey>,
): QueryObserverOptions<any, any, any, any, any> {
return super.getQueryDefaults(cloneDeepUnref(queryKey))
}

Expand All @@ -280,8 +280,8 @@ export class QueryClient extends QC {
}

getMutationDefaults(
mutationKey?: MaybeRefDeep<MutationKey>,
): MutationObserverOptions<any, any, any, any> | undefined {
mutationKey: MaybeRefDeep<MutationKey>,
): MutationObserverOptions<any, any, any, any> {
return super.getMutationDefaults(cloneDeepUnref(mutationKey))
}
}

0 comments on commit 95ae966

Please sign in to comment.