Skip to content

Commit a795211

Browse files
committed
feat: replace query instance with query observer
1 parent 58acca1 commit a795211

18 files changed

+616
-423
lines changed

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, deepEqual } from './utils'
1+
import { stableStringify, deepEqual } 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,15 +38,8 @@ 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,
4742
isDataEqual: deepEqual,
48-
useErrorBoundary: false,
49-
},
50-
mutations: {
51-
throwOnError: false,
52-
useErrorBoundary: false,
5343
},
5444
}
5545

src/core/query.ts

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import {
66
noop,
77
Console,
88
getStatusProps,
9-
shallowEqual,
109
Updater,
1110
} from './utils'
12-
import { QueryInstance, OnStateUpdateFunction } from './queryInstance'
1311
import {
1412
ArrayQueryKey,
1513
InfiniteQueryConfig,
@@ -19,7 +17,8 @@ import {
1917
QueryFunction,
2018
QueryStatus,
2119
} from './types'
22-
import { QueryCache } from './queryCache'
20+
import type { QueryCache } from './queryCache'
21+
import { QueryObserver, UpdateListener } from './queryObserver'
2322

2423
// TYPES
2524

@@ -39,7 +38,7 @@ export interface QueryState<TResult, TError> {
3938
isError: boolean
4039
isFetched: boolean
4140
isFetching: boolean
42-
isFetchingMore?: IsFetchingMoreValue
41+
isFetchingMore: IsFetchingMoreValue
4342
isIdle: boolean
4443
isLoading: boolean
4544
isStale: boolean
@@ -111,7 +110,7 @@ export class Query<TResult, TError> {
111110
queryKey: ArrayQueryKey
112111
queryHash: string
113112
config: QueryConfig<TResult, TError>
114-
instances: QueryInstance<TResult, TError>[]
113+
observers: QueryObserver<TResult, TError>[]
115114
state: QueryState<TResult, TError>
116115
shouldContinueRetryOnFocus?: boolean
117116
promise?: Promise<TResult | undefined>
@@ -131,17 +130,14 @@ export class Query<TResult, TError> {
131130
this.queryKey = init.queryKey
132131
this.queryHash = init.queryHash
133132
this.notifyGlobalListeners = init.notifyGlobalListeners
134-
this.instances = []
133+
this.observers = []
135134
this.state = getDefaultState(init.config)
136135

137136
if (init.config.infinite) {
138137
const infiniteConfig = init.config as InfiniteQueryConfig<TResult, TError>
139138
const infiniteData = (this.state.data as unknown) as TResult[] | undefined
140139

141-
if (
142-
typeof infiniteData !== 'undefined' &&
143-
typeof this.state.canFetchMore === 'undefined'
144-
) {
140+
if (typeof infiniteData !== 'undefined') {
145141
this.fetchMoreVariable = infiniteConfig.getFetchMore(
146142
infiniteData[infiniteData.length - 1],
147143
infiniteData
@@ -154,31 +150,39 @@ export class Query<TResult, TError> {
154150
this.pageVariables = [[...this.queryKey]]
155151
}
156152
}
157-
}
158153

159-
private dispatch(action: Action<TResult, TError>): void {
160-
const newState = queryReducer(this.state, action)
154+
// If the query started with data, schedule
155+
// a stale timeout
156+
if (!isServer && this.state.data) {
157+
this.scheduleStaleTimeout()
158+
159+
// Simulate a query healing process
160+
this.heal()
161161

162-
// Only update state if something has changed
163-
if (!shallowEqual(this.state, newState)) {
164-
this.state = newState
165-
this.instances.forEach(d => d.onStateUpdate(newState, action))
166-
this.notifyGlobalListeners(this)
162+
// Schedule for garbage collection in case
163+
// nothing subscribes to this query
164+
this.scheduleGarbageCollection()
167165
}
168166
}
169167

168+
updateConfig(config: QueryConfig<TResult, TError>): void {
169+
this.config = config
170+
}
171+
172+
private dispatch(action: Action<TResult, TError>): void {
173+
this.state = queryReducer(this.state, action)
174+
this.observers.forEach(d => d.onQueryUpdate(this.state, action))
175+
this.notifyGlobalListeners(this)
176+
}
177+
170178
scheduleStaleTimeout(): void {
171179
if (isServer) {
172180
return
173181
}
174182

175183
this.clearStaleTimeout()
176184

177-
if (this.state.isStale) {
178-
return
179-
}
180-
181-
if (this.config.staleTime === Infinity) {
185+
if (this.state.isStale || this.config.staleTime === Infinity) {
182186
return
183187
}
184188

@@ -190,10 +194,6 @@ export class Query<TResult, TError> {
190194
invalidate(): void {
191195
this.clearStaleTimeout()
192196

193-
if (!this.queryCache.queries[this.queryHash]) {
194-
return
195-
}
196-
197197
if (this.state.isStale) {
198198
return
199199
}
@@ -202,12 +202,12 @@ export class Query<TResult, TError> {
202202
}
203203

204204
scheduleGarbageCollection(): void {
205-
this.clearCacheTimeout()
206-
207-
if (!this.queryCache.queries[this.queryHash]) {
205+
if (isServer) {
208206
return
209207
}
210208

209+
this.clearCacheTimeout()
210+
211211
if (this.config.cacheTime === Infinity) {
212212
return
213213
}
@@ -249,9 +249,9 @@ export class Query<TResult, TError> {
249249
delete this.promise
250250
}
251251

252-
clearIntervals(): void {
253-
this.instances.forEach(instance => {
254-
instance.clearInterval()
252+
private clearTimersObservers(): void {
253+
this.observers.forEach(observer => {
254+
observer.clearRefetchInterval()
255255
})
256256
}
257257

@@ -301,19 +301,57 @@ export class Query<TResult, TError> {
301301
this.clearStaleTimeout()
302302
this.clearCacheTimeout()
303303
this.clearRetryTimeout()
304-
this.clearIntervals()
304+
this.clearTimersObservers()
305305
this.cancel()
306306
delete this.queryCache.queries[this.queryHash]
307307
this.notifyGlobalListeners(this)
308308
}
309309

310+
isEnabled(): boolean {
311+
return this.observers.some(observer => observer.config.enabled)
312+
}
313+
314+
shouldRefetchOnWindowFocus(): boolean {
315+
return (
316+
this.isEnabled() &&
317+
this.state.isStale &&
318+
this.observers.some(observer => observer.config.refetchOnWindowFocus)
319+
)
320+
}
321+
310322
subscribe(
311-
onStateUpdate?: OnStateUpdateFunction<TResult, TError>
312-
): QueryInstance<TResult, TError> {
313-
const instance = new QueryInstance(this, onStateUpdate)
314-
this.instances.push(instance)
323+
listener?: UpdateListener<TResult, TError>
324+
): QueryObserver<TResult, TError> {
325+
const observer = new QueryObserver<TResult, TError>({
326+
queryCache: this.queryCache,
327+
queryKey: this.queryKey,
328+
...this.config,
329+
})
330+
331+
observer.subscribe(listener)
332+
333+
return observer
334+
}
335+
336+
subscribeObserver(observer: QueryObserver<TResult, TError>): void {
337+
this.observers.push(observer)
315338
this.heal()
316-
return instance
339+
}
340+
341+
unsubscribeObserver(
342+
observer: QueryObserver<TResult, TError>,
343+
preventGC?: boolean
344+
): void {
345+
this.observers = this.observers.filter(x => x !== observer)
346+
347+
if (!this.observers.length) {
348+
this.cancel()
349+
350+
if (!preventGC) {
351+
// Schedule garbage collection
352+
this.scheduleGarbageCollection()
353+
}
354+
}
317355
}
318356

319357
// Set up the core fetcher function
@@ -323,7 +361,11 @@ export class Query<TResult, TError> {
323361
): Promise<TResult> {
324362
try {
325363
// Perform the query
326-
const promiseOrValue = fn(...this.config.queryFnParamsFilter!(args))
364+
const filter = this.config.queryFnParamsFilter
365+
const params = filter ? filter(args) : args
366+
367+
// Perform the query
368+
const promiseOrValue = fn(...params)
327369

328370
this.cancelPromises = () => (promiseOrValue as any)?.cancel?.()
329371

@@ -502,8 +544,10 @@ export class Query<TResult, TError> {
502544
this.cancelled = null
503545

504546
try {
505-
// Set up the query refreshing state
506-
this.dispatch({ type: ActionType.Fetch })
547+
// Set to fetching state if not already in it
548+
if (!this.state.isFetching) {
549+
this.dispatch({ type: ActionType.Fetch })
550+
}
507551

508552
// Try to get the data
509553
const data = await this.tryFetchData(queryFn!, this.queryKey)
@@ -573,6 +617,7 @@ function getDefaultState<TResult, TError>(
573617
error: null,
574618
isFetched: false,
575619
isFetching: initialStatus === QueryStatus.Loading,
620+
isFetchingMore: false,
576621
failureCount: 0,
577622
isStale,
578623
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)