Skip to content

Commit 1d34599

Browse files
committed
fix: should be able to invalidate queries
1 parent 34a7c1c commit 1d34599

File tree

4 files changed

+162
-52
lines changed

4 files changed

+162
-52
lines changed

src/core/query.ts

Lines changed: 21 additions & 0 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
@@ -236,6 +243,7 @@ export class Query<TResult, TError> {
236243

237244
isStaleByTime(staleTime = 0): boolean {
238245
return (
246+
this.state.isInvalidated ||
239247
this.state.status !== QueryStatus.Success ||
240248
this.state.updatedAt + staleTime <= Date.now()
241249
)
@@ -295,6 +303,12 @@ export class Query<TResult, TError> {
295303
}
296304
}
297305

306+
invalidate(): void {
307+
if (!this.state.isInvalidated) {
308+
this.dispatch({ type: ActionType.Invalidate })
309+
}
310+
}
311+
298312
async refetch(
299313
options?: RefetchOptions,
300314
config?: ResolvedQueryConfig<TResult, TError>
@@ -610,6 +624,7 @@ function getDefaultState<TResult, TError>(
610624
isFetching: status === QueryStatus.Loading,
611625
isFetchingMore: false,
612626
isInitialData: true,
627+
isInvalidated: false,
613628
status,
614629
updateCount: 0,
615630
updatedAt: Date.now(),
@@ -647,6 +662,7 @@ export function queryReducer<TResult, TError>(
647662
isFetching: false,
648663
isFetchingMore: false,
649664
isInitialData: false,
665+
isInvalidated: false,
650666
status: QueryStatus.Success,
651667
updateCount: state.updateCount + 1,
652668
updatedAt: action.updatedAt ?? Date.now(),
@@ -662,6 +678,11 @@ export function queryReducer<TResult, TError>(
662678
throwInErrorBoundary: true,
663679
updateCount: state.updateCount + 1,
664680
}
681+
case ActionType.Invalidate:
682+
return {
683+
...state,
684+
isInvalidated: true,
685+
}
665686
default:
666687
return state
667688
}

src/core/queryCache.ts

Lines changed: 57 additions & 22 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,9 +35,14 @@ interface PrefetchQueryOptions {
3535
throwOnError?: boolean
3636
}
3737

38-
interface InvalidateQueriesOptions extends QueryPredicateOptions {
38+
interface RefetchQueriesOptions extends QueryPredicateOptions {
3939
refetchActive?: boolean
40+
refetchFresh?: boolean
4041
refetchInactive?: boolean
42+
refetchStale?: boolean
43+
}
44+
45+
interface InvalidateQueriesOptions extends RefetchQueriesOptions {
4146
throwOnError?: boolean
4247
}
4348

@@ -186,30 +191,60 @@ export class QueryCache {
186191
})
187192
}
188193

189-
async invalidateQueries(
194+
/**
195+
* @return Promise resolving to an array with the invalidated queries.
196+
*/
197+
invalidateQueries(
190198
predicate?: QueryPredicate,
191199
options?: InvalidateQueriesOptions
192-
): Promise<void> {
193-
const { refetchActive = true, refetchInactive = false, throwOnError } =
194-
options || {}
200+
): Promise<Query<unknown, unknown>[]> {
201+
const queries = this.getQueries(predicate, options)
195202

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-
}
203+
queries.forEach(query => {
204+
query.invalidate()
205+
})
206+
207+
let promise = this.refetchQueries(predicate, options)
208+
209+
if (!options?.throwOnError) {
210+
promise = promise.catch(() => queries)
212211
}
212+
213+
return promise.then(() => queries)
214+
}
215+
216+
/**
217+
* @return Promise resolving to an array with the refetched queries.
218+
*/
219+
refetchQueries(
220+
predicate?: QueryPredicate,
221+
options?: RefetchQueriesOptions
222+
): Promise<Query<unknown, unknown>[]> {
223+
const {
224+
refetchActive = true,
225+
refetchFresh = true,
226+
refetchInactive = false,
227+
refetchStale = true,
228+
} = options || {}
229+
230+
const queries: Query<unknown, unknown>[] = []
231+
const promises: Promise<unknown>[] = []
232+
233+
this.getQueries(predicate, options).forEach(query => {
234+
const stale = query.isStale()
235+
const enabled = query.isEnabled()
236+
if (
237+
((refetchActive && enabled) || (refetchInactive && !enabled)) &&
238+
((refetchStale && stale) || (refetchFresh && !stale))
239+
) {
240+
queries.push(query)
241+
promises.push(query.fetch())
242+
}
243+
})
244+
245+
return Promise.all(promises)
246+
.then(() => queries)
247+
.catch(() => queries)
213248
}
214249

215250
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/react/tests/useQuery.test.tsx

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,52 @@ describe('useQuery', () => {
487487
return null
488488
})
489489

490+
it('should update query stale state when invalidated with invalidateQueries', async () => {
491+
const key = queryKey()
492+
const states: QueryResult<string>[] = []
493+
494+
function Page() {
495+
const state = useQuery(key, () => 'data', { staleTime: Infinity })
496+
497+
states.push(state)
498+
499+
React.useEffect(() => {
500+
setTimeout(() => {
501+
queryCache.invalidateQueries(key, {
502+
refetchActive: false,
503+
refetchInactive: false,
504+
})
505+
}, 10)
506+
}, [])
507+
508+
return null
509+
}
510+
511+
render(<Page />)
512+
513+
await waitForMs(100)
514+
515+
expect(states.length).toBe(3)
516+
expect(states[0]).toMatchObject({
517+
data: undefined,
518+
isFetching: true,
519+
isSuccess: false,
520+
isStale: true,
521+
})
522+
expect(states[1]).toMatchObject({
523+
data: 'data',
524+
isFetching: false,
525+
isSuccess: true,
526+
isStale: false,
527+
})
528+
expect(states[2]).toMatchObject({
529+
data: 'data',
530+
isFetching: false,
531+
isSuccess: true,
532+
isStale: true,
533+
})
534+
})
535+
490536
it('should update disabled query when updated with invalidateQueries', async () => {
491537
const key = queryKey()
492538
const states: QueryResult<number>[] = []
@@ -516,25 +562,27 @@ describe('useQuery', () => {
516562

517563
render(<Page />)
518564

519-
await waitFor(() => expect(states.length).toBe(3))
565+
await waitForMs(100)
520566

521-
expect(states).toMatchObject([
522-
{
523-
data: undefined,
524-
isFetching: false,
525-
isSuccess: false,
526-
},
527-
{
528-
data: undefined,
529-
isFetching: true,
530-
isSuccess: false,
531-
},
532-
{
533-
data: 1,
534-
isFetching: false,
535-
isSuccess: true,
536-
},
537-
])
567+
expect(states.length).toBe(3)
568+
expect(states[0]).toMatchObject({
569+
data: undefined,
570+
isFetching: false,
571+
isSuccess: false,
572+
isStale: true,
573+
})
574+
expect(states[1]).toMatchObject({
575+
data: undefined,
576+
isFetching: true,
577+
isSuccess: false,
578+
isStale: true,
579+
})
580+
expect(states[2]).toMatchObject({
581+
data: 1,
582+
isFetching: false,
583+
isSuccess: true,
584+
isStale: true,
585+
})
538586
})
539587

540588
it('should keep the previous data when keepPreviousData is set', async () => {
@@ -910,13 +958,11 @@ describe('useQuery', () => {
910958

911959
render(<Page />)
912960

913-
await waitFor(() =>
914-
expect(states).toMatchObject([
915-
{ isStale: true },
916-
{ isStale: false },
917-
{ isStale: true },
918-
])
919-
)
961+
await waitForMs(100)
962+
963+
expect(states[0]).toMatchObject({ isStale: true })
964+
expect(states[1]).toMatchObject({ isStale: false })
965+
expect(states[2]).toMatchObject({ isStale: true })
920966
})
921967

922968
it('should notify query cache when a query becomes stale', async () => {

0 commit comments

Comments
 (0)