Skip to content

Commit 75a1d3d

Browse files
authored
feat: replace query instance with query observer (#867)
1 parent dc70f7d commit 75a1d3d

19 files changed

+710
-417
lines changed

docs/src/pages/docs/api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
retryDelay,
2828
staleTime,
2929
cacheTime,
30+
keepPreviousData,
3031
refetchOnWindowFocus,
3132
refetchInterval,
3233
refetchIntervalInBackground,
@@ -112,6 +113,10 @@ const queryInfo = useQuery({
112113
- Optional
113114
- If set, this will mark any `initialData` provided as stale and will likely cause it to be refetched on mount
114115
- If a function is passed, it will be called only when appropriate to resolve the `initialStale` value. This can be useful if your `initialStale` value is costly to calculate.
116+
- `keepPreviousData: Boolean`
117+
- Optional
118+
- Defaults to `false`
119+
- If set, any previous `data` will be kept when fetching new data because the query key changed.
115120
- `refetchOnMount: Boolean`
116121
- Optional
117122
- Defaults to `true`

src/core/config.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { stableStringify, identity } from './utils'
1+
import { stableStringify } from './utils'
22
import {
33
ArrayQueryKey,
44
QueryKey,
@@ -30,9 +30,6 @@ export const defaultQueryKeySerializerFn: QueryKeySerializerFunction = (
3030
}
3131

3232
export const DEFAULT_CONFIG: ReactQueryConfig = {
33-
shared: {
34-
suspense: false,
35-
},
3633
queries: {
3734
queryKeySerializerFn: defaultQueryKeySerializerFn,
3835
enabled: true,
@@ -41,14 +38,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
4138
staleTime: 0,
4239
cacheTime: 5 * 60 * 1000,
4340
refetchOnWindowFocus: true,
44-
refetchInterval: false,
45-
queryFnParamsFilter: identity,
4641
refetchOnMount: true,
47-
useErrorBoundary: false,
48-
},
49-
mutations: {
50-
throwOnError: false,
51-
useErrorBoundary: false,
5242
},
5343
}
5444

src/core/query.ts

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
Updater,
1010
replaceEqualDeep,
1111
} from './utils'
12-
import { QueryInstance, OnStateUpdateFunction } from './queryInstance'
1312
import {
1413
ArrayQueryKey,
1514
InfiniteQueryConfig,
@@ -19,7 +18,8 @@ import {
1918
QueryFunction,
2019
QueryStatus,
2120
} from './types'
22-
import { QueryCache } from './queryCache'
21+
import type { QueryCache } from './queryCache'
22+
import { QueryObserver, UpdateListener } from './queryObserver'
2323

2424
// TYPES
2525

@@ -39,7 +39,7 @@ export interface QueryState<TResult, TError> {
3939
isError: boolean
4040
isFetched: boolean
4141
isFetching: boolean
42-
isFetchingMore?: IsFetchingMoreValue
42+
isFetchingMore: IsFetchingMoreValue
4343
isIdle: boolean
4444
isLoading: boolean
4545
isStale: boolean
@@ -111,7 +111,7 @@ export class Query<TResult, TError> {
111111
queryKey: ArrayQueryKey
112112
queryHash: string
113113
config: QueryConfig<TResult, TError>
114-
instances: QueryInstance<TResult, TError>[]
114+
observers: QueryObserver<TResult, TError>[]
115115
state: QueryState<TResult, TError>
116116
shouldContinueRetryOnFocus?: boolean
117117
promise?: Promise<TResult | undefined>
@@ -131,17 +131,14 @@ export class Query<TResult, TError> {
131131
this.queryKey = init.queryKey
132132
this.queryHash = init.queryHash
133133
this.notifyGlobalListeners = init.notifyGlobalListeners
134-
this.instances = []
134+
this.observers = []
135135
this.state = getDefaultState(init.config)
136136

137137
if (init.config.infinite) {
138138
const infiniteConfig = init.config as InfiniteQueryConfig<TResult, TError>
139139
const infiniteData = (this.state.data as unknown) as TResult[] | undefined
140140

141-
if (
142-
typeof infiniteData !== 'undefined' &&
143-
typeof this.state.canFetchMore === 'undefined'
144-
) {
141+
if (typeof infiniteData !== 'undefined') {
145142
this.fetchMoreVariable = infiniteConfig.getFetchMore(
146143
infiniteData[infiniteData.length - 1],
147144
infiniteData
@@ -154,11 +151,28 @@ export class Query<TResult, TError> {
154151
this.pageVariables = [[...this.queryKey]]
155152
}
156153
}
154+
155+
// If the query started with data, schedule
156+
// a stale timeout
157+
if (!isServer && this.state.data) {
158+
this.scheduleStaleTimeout()
159+
160+
// Simulate a query healing process
161+
this.heal()
162+
163+
// Schedule for garbage collection in case
164+
// nothing subscribes to this query
165+
this.scheduleGarbageCollection()
166+
}
167+
}
168+
169+
updateConfig(config: QueryConfig<TResult, TError>): void {
170+
this.config = config
157171
}
158172

159173
private dispatch(action: Action<TResult, TError>): void {
160174
this.state = queryReducer(this.state, action)
161-
this.instances.forEach(d => d.onStateUpdate(this.state, action))
175+
this.observers.forEach(d => d.onQueryUpdate(this.state, action))
162176
this.notifyGlobalListeners(this)
163177
}
164178

@@ -169,11 +183,7 @@ export class Query<TResult, TError> {
169183

170184
this.clearStaleTimeout()
171185

172-
if (this.state.isStale) {
173-
return
174-
}
175-
176-
if (this.config.staleTime === Infinity) {
186+
if (this.state.isStale || this.config.staleTime === Infinity) {
177187
return
178188
}
179189

@@ -185,10 +195,6 @@ export class Query<TResult, TError> {
185195
invalidate(): void {
186196
this.clearStaleTimeout()
187197

188-
if (!this.queryCache.queries[this.queryHash]) {
189-
return
190-
}
191-
192198
if (this.state.isStale) {
193199
return
194200
}
@@ -197,12 +203,12 @@ export class Query<TResult, TError> {
197203
}
198204

199205
scheduleGarbageCollection(): void {
200-
this.clearCacheTimeout()
201-
202-
if (!this.queryCache.queries[this.queryHash]) {
206+
if (isServer) {
203207
return
204208
}
205209

210+
this.clearCacheTimeout()
211+
206212
if (this.config.cacheTime === Infinity) {
207213
return
208214
}
@@ -244,9 +250,9 @@ export class Query<TResult, TError> {
244250
delete this.promise
245251
}
246252

247-
clearIntervals(): void {
248-
this.instances.forEach(instance => {
249-
instance.clearInterval()
253+
private clearTimersObservers(): void {
254+
this.observers.forEach(observer => {
255+
observer.clearRefetchInterval()
250256
})
251257
}
252258

@@ -310,19 +316,57 @@ export class Query<TResult, TError> {
310316
this.clearStaleTimeout()
311317
this.clearCacheTimeout()
312318
this.clearRetryTimeout()
313-
this.clearIntervals()
319+
this.clearTimersObservers()
314320
this.cancel()
315321
delete this.queryCache.queries[this.queryHash]
316322
this.notifyGlobalListeners(this)
317323
}
318324

325+
isEnabled(): boolean {
326+
return this.observers.some(observer => observer.config.enabled)
327+
}
328+
329+
shouldRefetchOnWindowFocus(): boolean {
330+
return (
331+
this.isEnabled() &&
332+
this.state.isStale &&
333+
this.observers.some(observer => observer.config.refetchOnWindowFocus)
334+
)
335+
}
336+
319337
subscribe(
320-
onStateUpdate?: OnStateUpdateFunction<TResult, TError>
321-
): QueryInstance<TResult, TError> {
322-
const instance = new QueryInstance(this, onStateUpdate)
323-
this.instances.push(instance)
338+
listener?: UpdateListener<TResult, TError>
339+
): QueryObserver<TResult, TError> {
340+
const observer = new QueryObserver<TResult, TError>({
341+
queryCache: this.queryCache,
342+
queryKey: this.queryKey,
343+
...this.config,
344+
})
345+
346+
observer.subscribe(listener)
347+
348+
return observer
349+
}
350+
351+
subscribeObserver(observer: QueryObserver<TResult, TError>): void {
352+
this.observers.push(observer)
324353
this.heal()
325-
return instance
354+
}
355+
356+
unsubscribeObserver(
357+
observer: QueryObserver<TResult, TError>,
358+
preventGC?: boolean
359+
): void {
360+
this.observers = this.observers.filter(x => x !== observer)
361+
362+
if (!this.observers.length) {
363+
this.cancel()
364+
365+
if (!preventGC) {
366+
// Schedule garbage collection
367+
this.scheduleGarbageCollection()
368+
}
369+
}
326370
}
327371

328372
// Set up the core fetcher function
@@ -332,7 +376,11 @@ export class Query<TResult, TError> {
332376
): Promise<TResult> {
333377
try {
334378
// Perform the query
335-
const promiseOrValue = fn(...this.config.queryFnParamsFilter!(args))
379+
const filter = this.config.queryFnParamsFilter
380+
const params = filter ? filter(args) : args
381+
382+
// Perform the query
383+
const promiseOrValue = fn(...params)
336384

337385
this.cancelPromises = () => (promiseOrValue as any)?.cancel?.()
338386

@@ -584,6 +632,7 @@ function getDefaultState<TResult, TError>(
584632
error: null,
585633
isFetched: false,
586634
isFetching: initialStatus === QueryStatus.Loading,
635+
isFetchingMore: false,
587636
failureCount: 0,
588637
isStale,
589638
data: initialData,

src/core/queryCache.ts

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ export class QueryCache {
121121
return this.configRef.current
122122
}
123123

124+
getDefaultedConfig<TResult, TError>(config?: QueryConfig<TResult, TError>) {
125+
return {
126+
...this.configRef.current.shared!,
127+
...this.configRef.current.queries!,
128+
queryCache: this,
129+
...config,
130+
} as QueryConfig<TResult, TError>
131+
}
132+
124133
subscribe(listener: QueryCacheListener): () => void {
125134
this.globalListeners.push(listener)
126135
return () => {
@@ -195,11 +204,8 @@ export class QueryCache {
195204
try {
196205
await Promise.all(
197206
this.getQueries(predicate, options).map(query => {
198-
if (query.instances.length) {
199-
if (
200-
refetchActive &&
201-
query.instances.some(instance => instance.config.enabled)
202-
) {
207+
if (query.observers.length) {
208+
if (refetchActive && query.isEnabled()) {
203209
return query.fetch()
204210
}
205211
} else {
@@ -226,21 +232,17 @@ export class QueryCache {
226232

227233
buildQuery<TResult, TError = unknown>(
228234
userQueryKey: QueryKey,
229-
queryConfig: QueryConfig<TResult, TError> = {}
235+
queryConfig?: QueryConfig<TResult, TError>
230236
): Query<TResult, TError> {
231-
const config = {
232-
...this.configRef.current.shared!,
233-
...this.configRef.current.queries!,
234-
...queryConfig,
235-
} as QueryConfig<TResult, TError>
237+
const config = this.getDefaultedConfig(queryConfig)
236238

237239
const [queryHash, queryKey] = config.queryKeySerializerFn!(userQueryKey)
238240

239241
let query
240242

241243
if (this.queries[queryHash]) {
242244
query = this.queries[queryHash] as Query<TResult, TError>
243-
query.config = config
245+
query.updateConfig(config)
244246
}
245247

246248
if (!query) {
@@ -254,18 +256,6 @@ export class QueryCache {
254256
},
255257
})
256258

257-
// If the query started with data, schedule
258-
// a stale timeout
259-
if (!isServer && query.state.data) {
260-
query.scheduleStaleTimeout()
261-
262-
// Simulate a query healing process
263-
query.heal()
264-
// Schedule for garbage collection in case
265-
// nothing subscribes to this query
266-
query.scheduleGarbageCollection()
267-
}
268-
269259
if (!this.config.frozen) {
270260
this.queries[queryHash] = query
271261

@@ -386,7 +376,7 @@ export class QueryCache {
386376
setQueryData<TResult, TError = unknown>(
387377
queryKey: QueryKey,
388378
updater: Updater<TResult | undefined, TResult>,
389-
config: QueryConfig<TResult, TError> = {}
379+
config?: QueryConfig<TResult, TError>
390380
) {
391381
let query = this.getQuery<TResult, TError>(queryKey)
392382

0 commit comments

Comments
 (0)