Skip to content

Commit f3b2fe0

Browse files
authored
feat: determine staleness locally instead of globally (#933)
1 parent d85f79b commit f3b2fe0

File tree

10 files changed

+275
-331
lines changed

10 files changed

+275
-331
lines changed

docs/src/pages/docs/comparison.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Feature/Capability Key:
1919
| Supported Query Keys | JSON | JSON | GraphQL Query |
2020
| Query Key Change Detection | Deep Compare (Serialization) | Referential Equality (===) | Deep Compare (Serialization) |
2121
| Query Data Memoization Level | Query + Structural Sharing | Query | Query + Entity + Structural Sharing |
22+
| Stale While Revalidate | Server-Side + Client-Side | Server-Side | None |
2223
| Bundle Size | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr] | [![][bp-apollo]][bpl-apollo] |
2324
| Queries ||||
2425
| Caching ||||

src/core/query.ts

Lines changed: 31 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export interface QueryState<TResult, TError> {
4444
isFetchingMore: IsFetchingMoreValue
4545
isIdle: boolean
4646
isLoading: boolean
47-
isStale: boolean
4847
isSuccess: boolean
4948
status: QueryStatus
5049
throwInErrorBoundary?: boolean
@@ -66,7 +65,6 @@ export interface RefetchOptions {
6665

6766
export enum ActionType {
6867
Failed = 'Failed',
69-
MarkStale = 'MarkStale',
7068
Fetch = 'Fetch',
7169
Success = 'Success',
7270
Error = 'Error',
@@ -76,10 +74,6 @@ interface FailedAction {
7674
type: ActionType.Failed
7775
}
7876

79-
interface MarkStaleAction {
80-
type: ActionType.MarkStale
81-
}
82-
8377
interface FetchAction {
8478
type: ActionType.Fetch
8579
isFetchingMore?: IsFetchingMoreValue
@@ -89,7 +83,6 @@ interface SuccessAction<TResult> {
8983
type: ActionType.Success
9084
data: TResult | undefined
9185
canFetchMore?: boolean
92-
isStale: boolean
9386
}
9487

9588
interface ErrorAction<TError> {
@@ -101,7 +94,6 @@ export type Action<TResult, TError> =
10194
| ErrorAction<TError>
10295
| FailedAction
10396
| FetchAction
104-
| MarkStaleAction
10597
| SuccessAction<TResult>
10698

10799
// CLASS
@@ -115,14 +107,11 @@ export class Query<TResult, TError> {
115107

116108
private queryCache: QueryCache
117109
private promise?: Promise<TResult | undefined>
118-
private cacheTimeout?: number
119-
private staleTimeout?: number
110+
private gcTimeout?: number
120111
private cancelFetch?: () => void
121112
private continueFetch?: () => void
122113
private isTransportCancelable?: boolean
123114
private notifyGlobalListeners: (query: Query<TResult, TError>) => void
124-
private enableStaleTimeout: boolean
125-
private enableGarbageCollectionTimeout: boolean
126115

127116
constructor(init: QueryInitConfig<TResult, TError>) {
128117
this.config = init.config
@@ -132,23 +121,7 @@ export class Query<TResult, TError> {
132121
this.notifyGlobalListeners = init.notifyGlobalListeners
133122
this.observers = []
134123
this.state = getDefaultState(init.config)
135-
this.enableStaleTimeout = false
136-
this.enableGarbageCollectionTimeout = false
137-
}
138-
139-
activateStaleTimeout(): void {
140-
this.enableStaleTimeout = true
141-
this.rescheduleStaleTimeout()
142-
}
143-
144-
activateGarbageCollectionTimeout(): void {
145-
this.enableGarbageCollectionTimeout = true
146-
this.rescheduleGarbageCollection()
147-
}
148-
149-
activateTimeouts(): void {
150-
this.activateStaleTimeout()
151-
this.activateGarbageCollectionTimeout()
124+
this.scheduleGc()
152125
}
153126

154127
updateConfig(config: QueryConfig<TResult, TError>): void {
@@ -161,61 +134,18 @@ export class Query<TResult, TError> {
161134
this.notifyGlobalListeners(this)
162135
}
163136

164-
private rescheduleStaleTimeout(): void {
137+
private scheduleGc(): void {
165138
if (isServer) {
166139
return
167140
}
168141

169-
this.clearStaleTimeout()
170-
171-
if (
172-
!this.enableStaleTimeout ||
173-
this.state.isStale ||
174-
this.state.status !== QueryStatus.Success ||
175-
this.config.staleTime === Infinity
176-
) {
177-
return
178-
}
179-
180-
const staleTime = this.config.staleTime || 0
181-
let timeout = staleTime
182-
if (this.state.updatedAt) {
183-
const timeElapsed = Date.now() - this.state.updatedAt
184-
const timeUntilStale = staleTime - timeElapsed
185-
timeout = Math.max(timeUntilStale, 0)
186-
}
187-
188-
this.staleTimeout = setTimeout(() => {
189-
this.invalidate()
190-
}, timeout)
191-
}
142+
this.clearGcTimeout()
192143

193-
invalidate(): void {
194-
this.clearStaleTimeout()
195-
196-
if (this.state.isStale) {
144+
if (this.config.cacheTime === Infinity || this.observers.length > 0) {
197145
return
198146
}
199147

200-
this.dispatch({ type: ActionType.MarkStale })
201-
}
202-
203-
private rescheduleGarbageCollection(): void {
204-
if (isServer) {
205-
return
206-
}
207-
208-
this.clearCacheTimeout()
209-
210-
if (
211-
!this.enableGarbageCollectionTimeout ||
212-
this.config.cacheTime === Infinity ||
213-
this.observers.length > 0
214-
) {
215-
return
216-
}
217-
218-
this.cacheTimeout = setTimeout(() => {
148+
this.gcTimeout = setTimeout(() => {
219149
this.clear()
220150
}, this.config.cacheTime)
221151
}
@@ -241,21 +171,14 @@ export class Query<TResult, TError> {
241171

242172
private clearTimersObservers(): void {
243173
this.observers.forEach(observer => {
244-
observer.clearRefetchInterval()
174+
observer.clearTimers()
245175
})
246176
}
247177

248-
private clearStaleTimeout() {
249-
if (this.staleTimeout) {
250-
clearTimeout(this.staleTimeout)
251-
this.staleTimeout = undefined
252-
}
253-
}
254-
255-
private clearCacheTimeout() {
256-
if (this.cacheTimeout) {
257-
clearTimeout(this.cacheTimeout)
258-
this.cacheTimeout = undefined
178+
private clearGcTimeout() {
179+
if (this.gcTimeout) {
180+
clearTimeout(this.gcTimeout)
181+
this.gcTimeout = undefined
259182
}
260183
}
261184

@@ -275,25 +198,19 @@ export class Query<TResult, TError> {
275198
data = prevData
276199
}
277200

278-
const isStale = this.config.staleTime === 0
279-
280201
// Try to determine if more data can be fetched
281202
const canFetchMore = hasMorePages(this.config, data)
282203

283204
// Set data and mark it as cached
284205
this.dispatch({
285206
type: ActionType.Success,
286207
data,
287-
isStale,
288208
canFetchMore,
289209
})
290-
291-
this.rescheduleStaleTimeout()
292210
}
293211

294212
clear(): void {
295-
this.clearStaleTimeout()
296-
this.clearCacheTimeout()
213+
this.clearGcTimeout()
297214
this.clearTimersObservers()
298215
this.cancel()
299216
delete this.queryCache.queries[this.queryHash]
@@ -304,12 +221,23 @@ export class Query<TResult, TError> {
304221
return this.observers.some(observer => observer.config.enabled)
305222
}
306223

224+
isStale(): boolean {
225+
return this.observers.some(observer => observer.isStale())
226+
}
227+
228+
isStaleByTime(staleTime = 0): boolean {
229+
return (
230+
!this.state.isSuccess || this.state.updatedAt + staleTime <= Date.now()
231+
)
232+
}
233+
307234
onWindowFocus(): void {
308235
if (
309-
this.state.isStale &&
310236
this.observers.some(
311237
observer =>
312-
observer.config.enabled && observer.config.refetchOnWindowFocus
238+
observer.isStale() &&
239+
observer.config.enabled &&
240+
observer.config.refetchOnWindowFocus
313241
)
314242
) {
315243
this.fetch()
@@ -319,10 +247,11 @@ export class Query<TResult, TError> {
319247

320248
onOnline(): void {
321249
if (
322-
this.state.isStale &&
323250
this.observers.some(
324251
observer =>
325-
observer.config.enabled && observer.config.refetchOnReconnect
252+
observer.isStale() &&
253+
observer.config.enabled &&
254+
observer.config.refetchOnReconnect
326255
)
327256
) {
328257
this.fetch()
@@ -348,7 +277,7 @@ export class Query<TResult, TError> {
348277
this.observers.push(observer)
349278

350279
// Stop the query from being garbage collected
351-
this.clearCacheTimeout()
280+
this.clearGcTimeout()
352281
}
353282

354283
unsubscribeObserver(observer: QueryObserver<TResult, TError>): void {
@@ -362,7 +291,7 @@ export class Query<TResult, TError> {
362291
}
363292
}
364293

365-
this.rescheduleGarbageCollection()
294+
this.scheduleGc()
366295
}
367296

368297
private async tryFetchData<T>(
@@ -639,12 +568,6 @@ function getDefaultState<TResult, TError>(
639568

640569
const hasInitialData = typeof initialData !== 'undefined'
641570

642-
const isStale =
643-
!config.enabled ||
644-
(typeof config.initialStale === 'function'
645-
? config.initialStale()
646-
: config.initialStale ?? !hasInitialData)
647-
648571
const initialStatus = hasInitialData
649572
? QueryStatus.Success
650573
: config.enabled
@@ -658,9 +581,8 @@ function getDefaultState<TResult, TError>(
658581
isFetching: initialStatus === QueryStatus.Loading,
659582
isFetchingMore: false,
660583
failureCount: 0,
661-
isStale,
662584
data: initialData,
663-
updatedAt: hasInitialData ? Date.now() : 0,
585+
updatedAt: Date.now(),
664586
canFetchMore: hasMorePages(config, initialData),
665587
}
666588
}
@@ -675,11 +597,6 @@ export function queryReducer<TResult, TError>(
675597
...state,
676598
failureCount: state.failureCount + 1,
677599
}
678-
case ActionType.MarkStale:
679-
return {
680-
...state,
681-
isStale: true,
682-
}
683600
case ActionType.Fetch:
684601
const status =
685602
typeof state.data !== 'undefined'
@@ -698,7 +615,6 @@ export function queryReducer<TResult, TError>(
698615
...getStatusProps(QueryStatus.Success),
699616
data: action.data,
700617
error: null,
701-
isStale: action.isStale,
702618
isFetched: true,
703619
isFetching: false,
704620
isFetchingMore: false,
@@ -714,7 +630,6 @@ export function queryReducer<TResult, TError>(
714630
isFetched: true,
715631
isFetching: false,
716632
isFetchingMore: false,
717-
isStale: true,
718633
failureCount: state.failureCount + 1,
719634
throwInErrorBoundary: true,
720635
}

src/core/queryCache.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export class QueryCache {
189189
}
190190
}
191191

192-
return query.invalidate()
192+
return undefined
193193
})
194194
)
195195
} catch (err) {
@@ -305,7 +305,7 @@ export class QueryCache {
305305
let query
306306
try {
307307
query = this.buildQuery<TResult, TError>(queryKey, configWithoutRetry)
308-
if (options?.force || query.state.isStale) {
308+
if (options?.force || query.isStaleByTime(config.staleTime)) {
309309
await query.fetch()
310310
}
311311
return query.state.data
@@ -314,14 +314,6 @@ export class QueryCache {
314314
throw error
315315
}
316316
return
317-
} finally {
318-
if (query) {
319-
// When prefetching, no observer is tied to the query,
320-
// so to avoid immediate garbage collection of the still
321-
// empty query, we wait with activating timeouts until
322-
// the prefetch is done
323-
query.activateTimeouts()
324-
}
325317
}
326318
}
327319

@@ -337,13 +329,11 @@ export class QueryCache {
337329
return
338330
}
339331

340-
const newQuery = this.buildQuery<TResult, TError>(queryKey, {
332+
this.buildQuery<TResult, TError>(queryKey, {
341333
initialStale: typeof config?.staleTime === 'undefined',
342334
initialData: functionalUpdate(updater, undefined),
343335
...config,
344336
})
345-
346-
newQuery.activateTimeouts()
347337
}
348338
}
349339

0 commit comments

Comments
 (0)