Skip to content

Commit c5738db

Browse files
authored
fix: should be able to invalidate queries (#1006)
1 parent 34a7c1c commit c5738db

File tree

5 files changed

+216
-60
lines changed

5 files changed

+216
-60
lines changed

src/core/query.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface QueryState<TResult, TError> {
3434
isFetching: boolean
3535
isFetchingMore: IsFetchingMoreValue
3636
isInitialData: boolean
37+
isInvalidated: boolean
3738
status: QueryStatus
3839
throwInErrorBoundary?: boolean
3940
updateCount: number
@@ -58,6 +59,7 @@ const enum ActionType {
5859
Fetch,
5960
Success,
6061
Error,
62+
Invalidate,
6163
}
6264

6365
interface SetDataOptions {
@@ -85,10 +87,15 @@ interface ErrorAction<TError> {
8587
error: TError
8688
}
8789

90+
interface InvalidateAction {
91+
type: ActionType.Invalidate
92+
}
93+
8894
export type Action<TResult, TError> =
8995
| ErrorAction<TError>
9096
| FailedAction
9197
| FetchAction
98+
| InvalidateAction
9299
| SuccessAction<TResult>
93100

94101
// CLASS
@@ -226,16 +233,21 @@ export class Query<TResult, TError> {
226233
this.cancel()
227234
}
228235

229-
isEnabled(): boolean {
236+
isActive(): boolean {
230237
return this.observers.some(observer => observer.config.enabled)
231238
}
232239

233240
isStale(): boolean {
234-
return this.observers.some(observer => observer.getCurrentResult().isStale)
241+
return (
242+
this.state.isInvalidated ||
243+
this.state.status !== QueryStatus.Success ||
244+
this.observers.some(observer => observer.getCurrentResult().isStale)
245+
)
235246
}
236247

237248
isStaleByTime(staleTime = 0): boolean {
238249
return (
250+
this.state.isInvalidated ||
239251
this.state.status !== QueryStatus.Success ||
240252
this.state.updatedAt + staleTime <= Date.now()
241253
)
@@ -295,6 +307,12 @@ export class Query<TResult, TError> {
295307
}
296308
}
297309

310+
invalidate(): void {
311+
if (!this.state.isInvalidated) {
312+
this.dispatch({ type: ActionType.Invalidate })
313+
}
314+
}
315+
298316
async refetch(
299317
options?: RefetchOptions,
300318
config?: ResolvedQueryConfig<TResult, TError>
@@ -610,6 +628,7 @@ function getDefaultState<TResult, TError>(
610628
isFetching: status === QueryStatus.Loading,
611629
isFetchingMore: false,
612630
isInitialData: true,
631+
isInvalidated: false,
613632
status,
614633
updateCount: 0,
615634
updatedAt: Date.now(),
@@ -647,6 +666,7 @@ export function queryReducer<TResult, TError>(
647666
isFetching: false,
648667
isFetchingMore: false,
649668
isInitialData: false,
669+
isInvalidated: false,
650670
status: QueryStatus.Success,
651671
updateCount: state.updateCount + 1,
652672
updatedAt: action.updatedAt ?? Date.now(),
@@ -662,6 +682,11 @@ export function queryReducer<TResult, TError>(
662682
throwInErrorBoundary: true,
663683
updateCount: state.updateCount + 1,
664684
}
685+
case ActionType.Invalidate:
686+
return {
687+
...state,
688+
isInvalidated: true,
689+
}
665690
default:
666691
return state
667692
}

src/core/queryCache.ts

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
deepIncludes,
44
getQueryArgs,
55
isDocumentVisible,
6-
isPlainObject,
76
isOnline,
7+
isPlainObject,
88
isServer,
99
} from './utils'
1010
import { getResolvedQueryConfig } from './config'
@@ -35,14 +35,19 @@ interface PrefetchQueryOptions {
3535
throwOnError?: boolean
3636
}
3737

38-
interface InvalidateQueriesOptions extends QueryPredicateOptions {
38+
interface RefetchQueriesOptions extends QueryPredicateOptions {
39+
throwOnError?: boolean
40+
}
41+
42+
interface InvalidateQueriesOptions extends RefetchQueriesOptions {
3943
refetchActive?: boolean
4044
refetchInactive?: boolean
41-
throwOnError?: boolean
4245
}
4346

4447
interface QueryPredicateOptions {
48+
active?: boolean
4549
exact?: boolean
50+
stale?: boolean
4651
}
4752

4853
type QueryPredicate = QueryKey | QueryPredicateFn | true
@@ -123,7 +128,7 @@ export class QueryCache {
123128
predicate?: QueryPredicate,
124129
options?: QueryPredicateOptions
125130
): Query<TResult, TError>[] {
126-
if (predicate === true || typeof predicate === 'undefined') {
131+
if (!options && (predicate === true || typeof predicate === 'undefined')) {
127132
return this.queriesArray
128133
}
129134

@@ -134,10 +139,19 @@ export class QueryCache {
134139
} else {
135140
const resolvedConfig = this.getResolvedQueryConfig(predicate)
136141

137-
predicateFn = d =>
138-
options?.exact
139-
? d.queryHash === resolvedConfig.queryHash
140-
: deepIncludes(d.queryKey, resolvedConfig.queryKey)
142+
predicateFn = query => {
143+
if (
144+
options &&
145+
((options.exact && query.queryHash !== resolvedConfig.queryHash) ||
146+
(typeof options.active === 'boolean' &&
147+
query.isActive() !== options.active) ||
148+
(typeof options.stale === 'boolean' &&
149+
query.isStale() !== options.stale))
150+
) {
151+
return false
152+
}
153+
return deepIncludes(query.queryKey, resolvedConfig.queryKey)
154+
}
141155
}
142156

143157
return this.queriesArray.filter(predicateFn)
@@ -186,30 +200,62 @@ export class QueryCache {
186200
})
187201
}
188202

189-
async invalidateQueries(
203+
/**
204+
* @return Promise resolving to an array with the invalidated queries.
205+
*/
206+
invalidateQueries(
190207
predicate?: QueryPredicate,
191208
options?: InvalidateQueriesOptions
192-
): Promise<void> {
193-
const { refetchActive = true, refetchInactive = false, throwOnError } =
194-
options || {}
209+
): Promise<Query<unknown, unknown>[]> {
210+
const queries = this.getQueries(predicate, options)
195211

196-
try {
197-
await Promise.all(
198-
this.getQueries(predicate, options).map(query => {
199-
const enabled = query.isEnabled()
200-
201-
if ((enabled && refetchActive) || (!enabled && refetchInactive)) {
202-
return query.fetch()
203-
}
204-
205-
return undefined
206-
})
207-
)
208-
} catch (err) {
209-
if (throwOnError) {
210-
throw err
211-
}
212+
queries.forEach(query => {
213+
query.invalidate()
214+
})
215+
216+
const { refetchActive = true, refetchInactive = false } = options || {}
217+
218+
if (!refetchInactive && !refetchActive) {
219+
return Promise.resolve(queries)
220+
}
221+
222+
const refetchOptions: RefetchQueriesOptions = { ...options }
223+
224+
if (refetchActive && !refetchInactive) {
225+
refetchOptions.active = true
226+
} else if (refetchInactive && !refetchActive) {
227+
refetchOptions.active = false
212228
}
229+
230+
let promise = this.refetchQueries(predicate, refetchOptions)
231+
232+
if (!options?.throwOnError) {
233+
promise = promise.catch(() => queries)
234+
}
235+
236+
return promise.then(() => queries)
237+
}
238+
239+
/**
240+
* @return Promise resolving to an array with the refetched queries.
241+
*/
242+
refetchQueries(
243+
predicate?: QueryPredicate,
244+
options?: RefetchQueriesOptions
245+
): Promise<Query<unknown, unknown>[]> {
246+
const promises: Promise<Query<unknown, unknown>>[] = []
247+
248+
this.getQueries(predicate, options).forEach(query => {
249+
let promise = query.fetch().then(() => query)
250+
251+
if (!options?.throwOnError) {
252+
promise = promise.catch(() => query)
253+
}
254+
255+
promises.push(promise)
256+
})
257+
258+
return Promise.all(promises)
213259
}
214260

215261
resetErrorBoundaries(): void {

src/core/queryObserver.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ export class QueryObserver<TResult, TError> {
306306
const { config } = this
307307
const { type } = action
308308

309-
// Update stale state on success or error
310-
if (type === 2 || type === 3) {
309+
// Update stale state on success, error or invalidation
310+
if (type === 2 || type === 3 || type === 4) {
311311
this.isStale = this.currentQuery.isStaleByTime(config.staleTime)
312312
}
313313

@@ -316,15 +316,23 @@ export class QueryObserver<TResult, TError> {
316316
this.updateResult()
317317
const currentResult = this.currentResult
318318

319-
// Trigger callbacks and timers on success or error
319+
// Update timers on success, error or invalidation
320+
if (type === 2 || type === 3 || type === 4) {
321+
this.updateTimers()
322+
}
323+
324+
// Trigger callbacks on success or error
320325
if (type === 2) {
321326
config.onSuccess?.(currentResult.data!)
322327
config.onSettled?.(currentResult.data!, null)
323-
this.updateTimers()
324328
} else if (type === 3) {
325329
config.onError?.(currentResult.error!)
326330
config.onSettled?.(undefined, currentResult.error!)
327-
this.updateTimers()
331+
}
332+
333+
// Do not notify if the query was invalidated but the stale state did not changed
334+
if (type === 4 && currentResult.isStale === prevResult.isStale) {
335+
return
328336
}
329337

330338
if (

src/core/tests/queryCache.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,37 @@ describe('queryCache', () => {
257257
expect(data).toEqual(['data1', 'data2'])
258258
})
259259

260+
test('getQueries should filter correctly', async () => {
261+
const key1 = queryKey()
262+
const key2 = queryKey()
263+
const cache = defaultQueryCache
264+
265+
await cache.prefetchQuery(key1, () => 'data1')
266+
await cache.prefetchQuery(key2, () => 'data2')
267+
await cache.invalidateQueries(key2)
268+
const query1 = cache.getQuery(key1)!
269+
const query2 = cache.getQuery(key2)!
270+
271+
expect(cache.getQueries(key1)).toEqual([query1])
272+
expect(cache.getQueries(key1, {})).toEqual([query1])
273+
expect(cache.getQueries(key1, { active: false })).toEqual([query1])
274+
expect(cache.getQueries(key1, { active: true })).toEqual([])
275+
expect(cache.getQueries(key1, { stale: true })).toEqual([])
276+
expect(cache.getQueries(key1, { stale: false })).toEqual([query1])
277+
expect(cache.getQueries(key1, { stale: false, active: true })).toEqual([])
278+
expect(cache.getQueries(key1, { stale: false, active: false })).toEqual([
279+
query1,
280+
])
281+
expect(
282+
cache.getQueries(key1, { stale: false, active: false, exact: true })
283+
).toEqual([query1])
284+
285+
expect(cache.getQueries(key2)).toEqual([query2])
286+
expect(cache.getQueries(key2, { stale: undefined })).toEqual([query2])
287+
expect(cache.getQueries(key2, { stale: true })).toEqual([query2])
288+
expect(cache.getQueries(key2, { stale: false })).toEqual([])
289+
})
290+
260291
test('query interval is cleared when unsubscribed to a refetchInterval query', async () => {
261292
const key = queryKey()
262293

0 commit comments

Comments
 (0)