Skip to content

Commit 4314ba5

Browse files
authored
refactor: define configuration merging strategy (TanStack#871)
1 parent e7432db commit 4314ba5

15 files changed

+177
-147
lines changed

src/core/config.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
QueryKey,
55
QueryKeySerializerFunction,
66
ReactQueryConfig,
7+
QueryConfig,
8+
MutationConfig,
79
} from './types'
810

911
// TYPES
@@ -29,6 +31,22 @@ export const defaultQueryKeySerializerFn: QueryKeySerializerFunction = (
2931
}
3032
}
3133

34+
/**
35+
* Config merging strategy
36+
*
37+
* When using hooks the config will be merged in the following order:
38+
*
39+
* 1. These defaults.
40+
* 2. Defaults from the hook query cache.
41+
* 3. Combined defaults from any config providers in the tree.
42+
* 4. Query/mutation config provided to the hook.
43+
*
44+
* When using a query cache directly the config will be merged in the following order:
45+
*
46+
* 1. These defaults.
47+
* 2. Defaults from the query cache.
48+
* 3. Query/mutation config provided to the query cache method.
49+
*/
3250
export const DEFAULT_CONFIG: ReactQueryConfig = {
3351
queries: {
3452
queryKeySerializerFn: defaultQueryKeySerializerFn,
@@ -42,6 +60,63 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
4260
},
4361
}
4462

45-
export const defaultConfigRef: ReactQueryConfigRef = {
46-
current: DEFAULT_CONFIG,
63+
export function mergeReactQueryConfigs(
64+
a: ReactQueryConfig,
65+
b: ReactQueryConfig
66+
): ReactQueryConfig {
67+
return {
68+
shared: {
69+
...a.shared,
70+
...b.shared,
71+
},
72+
queries: {
73+
...a.queries,
74+
...b.queries,
75+
},
76+
mutations: {
77+
...a.mutations,
78+
...b.mutations,
79+
},
80+
}
81+
}
82+
83+
export function getDefaultedQueryConfig<TResult, TError>(
84+
queryCacheConfig?: ReactQueryConfig,
85+
contextConfig?: ReactQueryConfig,
86+
config?: QueryConfig<TResult, TError>,
87+
configOverrides?: QueryConfig<TResult, TError>
88+
): QueryConfig<TResult, TError> {
89+
return {
90+
...DEFAULT_CONFIG.shared,
91+
...DEFAULT_CONFIG.queries,
92+
...queryCacheConfig?.shared,
93+
...queryCacheConfig?.queries,
94+
...contextConfig?.shared,
95+
...contextConfig?.queries,
96+
...config,
97+
...configOverrides,
98+
} as QueryConfig<TResult, TError>
99+
}
100+
101+
export function getDefaultedMutationConfig<
102+
TResult,
103+
TError,
104+
TVariables,
105+
TSnapshot
106+
>(
107+
queryCacheConfig?: ReactQueryConfig,
108+
contextConfig?: ReactQueryConfig,
109+
config?: MutationConfig<TResult, TError, TVariables, TSnapshot>,
110+
configOverrides?: MutationConfig<TResult, TError, TVariables, TSnapshot>
111+
): MutationConfig<TResult, TError, TVariables, TSnapshot> {
112+
return {
113+
...DEFAULT_CONFIG.shared,
114+
...DEFAULT_CONFIG.mutations,
115+
...queryCacheConfig?.shared,
116+
...queryCacheConfig?.mutations,
117+
...contextConfig?.shared,
118+
...contextConfig?.mutations,
119+
...config,
120+
...configOverrides,
121+
} as MutationConfig<TResult, TError, TVariables, TSnapshot>
47122
}

src/core/queryCache.ts

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
isObject,
77
Updater,
88
} from './utils'
9-
import { defaultConfigRef, ReactQueryConfigRef } from './config'
9+
import { getDefaultedQueryConfig } from './config'
1010
import { Query } from './query'
1111
import {
1212
QueryConfig,
@@ -76,7 +76,6 @@ export class QueryCache {
7676
isFetching: number
7777

7878
private config: QueryCacheConfig
79-
private configRef: ReactQueryConfigRef
8079
private globalListeners: QueryCacheListener[]
8180

8281
constructor(config?: QueryCacheConfig) {
@@ -85,25 +84,6 @@ export class QueryCache {
8584
// A frozen cache does not add new queries to the cache
8685
this.globalListeners = []
8786

88-
this.configRef = this.config.defaultConfig
89-
? {
90-
current: {
91-
shared: {
92-
...defaultConfigRef.current.shared,
93-
...this.config.defaultConfig.shared,
94-
},
95-
queries: {
96-
...defaultConfigRef.current.queries,
97-
...this.config.defaultConfig.queries,
98-
},
99-
mutations: {
100-
...defaultConfigRef.current.mutations,
101-
...this.config.defaultConfig.mutations,
102-
},
103-
},
104-
}
105-
: defaultConfigRef
106-
10787
this.queries = {}
10888
this.isFetching = 0
10989
}
@@ -118,16 +98,15 @@ export class QueryCache {
11898
}
11999

120100
getDefaultConfig() {
121-
return this.configRef.current
101+
return this.config.defaultConfig
122102
}
123103

124-
getDefaultedConfig<TResult, TError>(config?: QueryConfig<TResult, TError>) {
125-
return {
126-
...this.configRef.current.shared!,
127-
...this.configRef.current.queries!,
104+
getDefaultedQueryConfig<TResult, TError>(
105+
config?: QueryConfig<TResult, TError>
106+
): QueryConfig<TResult, TError> {
107+
return getDefaultedQueryConfig(this.getDefaultConfig(), undefined, config, {
128108
queryCache: this,
129-
...config,
130-
} as QueryConfig<TResult, TError>
109+
})
131110
}
132111

133112
subscribe(listener: QueryCacheListener): () => void {
@@ -158,8 +137,8 @@ export class QueryCache {
158137
if (typeof predicate === 'function') {
159138
predicateFn = predicate as QueryPredicateFn
160139
} else {
161-
const [queryHash, queryKey] = this.configRef.current.queries!
162-
.queryKeySerializerFn!(predicate)
140+
const config = this.getDefaultedQueryConfig()
141+
const [queryHash, queryKey] = config.queryKeySerializerFn!(predicate)
163142

164143
predicateFn = d =>
165144
options?.exact
@@ -234,7 +213,7 @@ export class QueryCache {
234213
userQueryKey: QueryKey,
235214
queryConfig?: QueryConfig<TResult, TError>
236215
): Query<TResult, TError> {
237-
const config = this.getDefaultedConfig(queryConfig)
216+
const config = this.getDefaultedQueryConfig(queryConfig)
238217

239218
const [queryHash, queryKey] = config.queryKeySerializerFn!(userQueryKey)
240219

@@ -352,6 +331,7 @@ export class QueryCache {
352331
TError,
353332
PrefetchQueryOptions | undefined
354333
>(args)
334+
355335
// https://github.com/tannerlinsley/react-query/issues/652
356336
const configWithoutRetry = { retry: false, ...config }
357337

src/core/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ export interface MutationConfig<
215215
onMutate?: (variables: TVariables) => Promise<TSnapshot> | TSnapshot
216216
useErrorBoundary?: boolean
217217
suspense?: boolean
218+
/**
219+
* By default the query cache from the context is used, but a different cache can be specified.
220+
*/
221+
queryCache?: QueryCache
218222
}
219223

220224
export type MutationFunction<TResult, TVariables = unknown> = (

src/react/ReactQueryConfigProvider.tsx

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,34 @@
11
import React from 'react'
2-
import { DEFAULT_CONFIG, defaultConfigRef } from '../core/config'
2+
3+
import { mergeReactQueryConfigs } from '../core/config'
34
import { ReactQueryConfig } from '../core/types'
4-
import { useQueryCache } from './ReactQueryCacheProvider'
55

66
const configContext = React.createContext<ReactQueryConfig | undefined>(
77
undefined
88
)
99

10-
export function useConfigContext() {
11-
const queryCache = useQueryCache()
12-
return (
13-
React.useContext(configContext) ||
14-
queryCache.getDefaultConfig() ||
15-
defaultConfigRef.current
16-
)
10+
export function useContextConfig() {
11+
return React.useContext(configContext)
1712
}
1813

19-
export interface ReactQueryProviderConfig extends ReactQueryConfig {}
20-
2114
export interface ReactQueryConfigProviderProps {
22-
config: ReactQueryProviderConfig
15+
config: ReactQueryConfig
2316
}
2417

2518
export const ReactQueryConfigProvider: React.FC<ReactQueryConfigProviderProps> = ({
2619
config,
2720
children,
2821
}) => {
29-
const configContextValueOrDefault = useConfigContext()
30-
const configContextValue = React.useContext(configContext)
22+
const parentConfig = useContextConfig()
3123

32-
const newConfig = React.useMemo<ReactQueryConfig>(() => {
33-
const { shared = {}, queries = {}, mutations = {} } = config
34-
const {
35-
shared: contextShared = {},
36-
queries: contextQueries = {},
37-
mutations: contextMutations = {},
38-
} = configContextValueOrDefault
39-
40-
return {
41-
shared: {
42-
...contextShared,
43-
...shared,
44-
},
45-
queries: {
46-
...contextQueries,
47-
...queries,
48-
},
49-
mutations: {
50-
...contextMutations,
51-
...mutations,
52-
},
53-
}
54-
}, [config, configContextValueOrDefault])
55-
56-
React.useEffect(() => {
57-
// restore previous config on unmount
58-
return () => {
59-
defaultConfigRef.current = {
60-
...(configContextValueOrDefault || DEFAULT_CONFIG),
61-
}
62-
}
63-
}, [configContextValueOrDefault])
64-
65-
// If this is the outermost provider, overwrite the shared default config
66-
if (!configContextValue) {
67-
defaultConfigRef.current = newConfig
68-
}
24+
const mergedConfig = React.useMemo(
25+
() =>
26+
parentConfig ? mergeReactQueryConfigs(parentConfig, config) : config,
27+
[config, parentConfig]
28+
)
6929

7030
return (
71-
<configContext.Provider value={newConfig}>
31+
<configContext.Provider value={mergedConfig}>
7232
{children}
7333
</configContext.Provider>
7434
)

src/react/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,4 @@ export type { UseQueryObjectConfig } from './useQuery'
1717
export type { UseInfiniteQueryObjectConfig } from './useInfiniteQuery'
1818
export type { UsePaginatedQueryObjectConfig } from './usePaginatedQuery'
1919
export type { ReactQueryCacheProviderProps } from './ReactQueryCacheProvider'
20-
export type {
21-
ReactQueryConfigProviderProps,
22-
ReactQueryProviderConfig,
23-
} from './ReactQueryConfigProvider'
20+
export type { ReactQueryConfigProviderProps } from './ReactQueryConfigProvider'

src/react/tests/ReactQueryConfigProvider.test.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ describe('ReactQueryConfigProvider', () => {
4242
})
4343

4444
it('should allow overriding the default config from the outermost provider', async () => {
45-
const key = queryKey()
45+
const key1 = queryKey()
46+
const key2 = queryKey()
4647

4748
const outerConfig = {
4849
queries: {
@@ -65,25 +66,28 @@ describe('ReactQueryConfigProvider', () => {
6566
function Container() {
6667
return (
6768
<ReactQueryConfigProvider config={outerConfig}>
69+
<First />
6870
<ReactQueryConfigProvider config={innerConfig}>
69-
<h1>Placeholder</h1>
71+
<Second />
7072
</ReactQueryConfigProvider>
7173
</ReactQueryConfigProvider>
7274
)
7375
}
7476

75-
const rendered = render(<Container />)
76-
77-
await waitFor(() => rendered.getByText('Placeholder'))
78-
79-
await queryCache.prefetchQuery(key)
77+
function First() {
78+
const { data } = useQuery(key1)
79+
return <span>First: {String(data)}</span>
80+
}
8081

81-
expect(outerConfig.queries.queryFn).toHaveBeenCalledWith(key)
82-
expect(innerConfig.queries.queryFn).not.toHaveBeenCalled()
82+
function Second() {
83+
const { data } = useQuery(key2)
84+
return <span>Second: {String(data)}</span>
85+
}
8386

84-
const data = queryCache.getQueryData(key)
87+
const rendered = render(<Container />)
8588

86-
expect(data).toEqual('outer')
89+
await waitFor(() => rendered.getByText('First: outer'))
90+
await waitFor(() => rendered.getByText('Second: inner'))
8791
})
8892

8993
it('should reset to defaults when unmounted', async () => {

src/react/tests/useInfiniteQuery.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ describe('useInfiniteQuery', () => {
134134
React.useEffect(() => {
135135
setTimeout(() => {
136136
fetchMore()
137-
}, 20)
137+
}, 50)
138138
setTimeout(() => {
139139
setOrder('asc')
140-
}, 40)
140+
}, 100)
141141
}, [fetchMore])
142142

143143
return null

src/react/useBaseQuery.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import React from 'react'
33
import { useRerenderer } from './utils'
44
import { QueryObserver } from '../core/queryObserver'
55
import { QueryResultBase, QueryObserverConfig } from '../core/types'
6+
import { useDefaultedQueryConfig } from './useDefaultedQueryConfig'
67

78
export function useBaseQuery<TResult, TError>(
89
config: QueryObserverConfig<TResult, TError> = {}
910
): QueryResultBase<TResult, TError> {
11+
config = useDefaultedQueryConfig(config)
12+
1013
// Make a rerender function
1114
const rerender = useRerenderer()
1215

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { MutationConfig } from '../core/types'
2+
import { getDefaultedMutationConfig } from '../core/config'
3+
import { useQueryCache } from './ReactQueryCacheProvider'
4+
import { useContextConfig } from './ReactQueryConfigProvider'
5+
6+
export function useDefaultedMutationConfig<
7+
TResult,
8+
TError,
9+
TVariables,
10+
TSnapshot
11+
>(
12+
config?: MutationConfig<TResult, TError, TVariables, TSnapshot>
13+
): MutationConfig<TResult, TError, TVariables, TSnapshot> {
14+
const contextConfig = useContextConfig()
15+
const contextQueryCache = useQueryCache()
16+
const queryCache = config?.queryCache || contextQueryCache
17+
const queryCacheConfig = queryCache.getDefaultConfig()
18+
return getDefaultedMutationConfig(queryCacheConfig, contextConfig, config, {
19+
queryCache,
20+
})
21+
}

0 commit comments

Comments
 (0)