Skip to content

Commit a3c36a5

Browse files
committed
fix: should be able to invalidate queries
1 parent 460ab33 commit a3c36a5

File tree

4 files changed

+143
-46
lines changed

4 files changed

+143
-46
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
@@ -226,6 +233,7 @@ export class Query<TResult, TError> {
226233

227234
isStaleByTime(staleTime = 0): boolean {
228235
return (
236+
this.state.isInvalidated ||
229237
this.state.status !== QueryStatus.Success ||
230238
this.state.updatedAt + staleTime <= Date.now()
231239
)
@@ -279,6 +287,12 @@ export class Query<TResult, TError> {
279287
}
280288
}
281289

290+
invalidate(): void {
291+
if (!this.state.isInvalidated) {
292+
this.dispatch({ type: ActionType.Invalidate })
293+
}
294+
}
295+
282296
async refetch(
283297
options?: RefetchOptions,
284298
config?: ResolvedQueryConfig<TResult, TError>
@@ -594,6 +608,7 @@ function getDefaultState<TResult, TError>(
594608
isFetching: status === QueryStatus.Loading,
595609
isFetchingMore: false,
596610
isInitialData: true,
611+
isInvalidated: false,
597612
status,
598613
updateCount: 0,
599614
updatedAt: Date.now(),
@@ -631,6 +646,7 @@ export function queryReducer<TResult, TError>(
631646
isFetching: false,
632647
isFetchingMore: false,
633648
isInitialData: false,
649+
isInvalidated: false,
634650
status: QueryStatus.Success,
635651
updateCount: state.updateCount + 1,
636652
updatedAt: action.updatedAt ?? Date.now(),
@@ -646,6 +662,11 @@ export function queryReducer<TResult, TError>(
646662
throwInErrorBoundary: true,
647663
updateCount: state.updateCount + 1,
648664
}
665+
case ActionType.Invalidate:
666+
return {
667+
...state,
668+
isInvalidated: true,
669+
}
649670
default:
650671
return state
651672
}

src/core/queryCache.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

@@ -190,28 +195,45 @@ export class QueryCache {
190195
predicate?: QueryPredicate,
191196
options?: InvalidateQueriesOptions
192197
): Promise<void> {
193-
const { refetchActive = true, refetchInactive = false, throwOnError } =
194-
options || {}
195-
198+
this.getQueries(predicate, options).forEach(query => {
199+
query.invalidate()
200+
})
196201
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-
)
202+
await this.refetchQueries(predicate, options)
208203
} catch (err) {
209-
if (throwOnError) {
204+
if (options?.throwOnError) {
210205
throw err
211206
}
212207
}
213208
}
214209

210+
refetchQueries(
211+
predicate?: QueryPredicate,
212+
options?: RefetchQueriesOptions
213+
): Promise<unknown[]> {
214+
const {
215+
refetchActive = true,
216+
refetchFresh = true,
217+
refetchInactive = false,
218+
refetchStale = true,
219+
} = options || {}
220+
221+
const promises: Promise<unknown>[] = []
222+
223+
this.getQueries(predicate, options).forEach(query => {
224+
const stale = query.isStale()
225+
const enabled = query.isEnabled()
226+
if (
227+
((refetchActive && enabled) || (refetchInactive && !enabled)) &&
228+
((refetchStale && stale) || (refetchFresh && !stale))
229+
) {
230+
promises.push(query.fetch())
231+
}
232+
})
233+
234+
return Promise.all(promises)
235+
}
236+
215237
resetErrorBoundaries(): void {
216238
this.getQueries().forEach(query => {
217239
query.state.throwInErrorBoundary = false

src/core/queryObserver.ts

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

298-
// Update stale state on success or error
299-
if (type === 2 || type === 3) {
298+
// Update stale state on success, error or invalidation
299+
if (type === 2 || type === 3 || type === 4) {
300300
this.isStale = this.currentQuery.isStaleByTime(config.staleTime)
301301
}
302302

@@ -305,15 +305,23 @@ export class QueryObserver<TResult, TError> {
305305
this.updateResult()
306306
const currentResult = this.currentResult
307307

308-
// Trigger callbacks and timers on success or error
308+
// Update timers on success, error or invalidation
309+
if (type === 2 || type === 3 || type === 4) {
310+
this.updateTimers()
311+
}
312+
313+
// Trigger callbacks on success or error
309314
if (type === 2) {
310315
config.onSuccess?.(currentResult.data!)
311316
config.onSettled?.(currentResult.data!, null)
312-
this.updateTimers()
313317
} else if (type === 3) {
314318
config.onError?.(currentResult.error!)
315319
config.onSettled?.(undefined, currentResult.error!)
316-
this.updateTimers()
320+
}
321+
322+
// Do not notify if the query was invalidated but the stale state did not changed
323+
if (type === 4 && currentResult.isStale === prevResult.isStale) {
324+
return
317325
}
318326

319327
if (

src/react/tests/useQuery.test.tsx

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,52 @@ describe('useQuery', () => {
482482
return null
483483
})
484484

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

512558
render(<Page />)
513559

514-
await waitFor(() => expect(states.length).toBe(3))
560+
await waitForMs(100)
515561

516-
expect(states).toMatchObject([
517-
{
518-
data: undefined,
519-
isFetching: false,
520-
isSuccess: false,
521-
},
522-
{
523-
data: undefined,
524-
isFetching: true,
525-
isSuccess: false,
526-
},
527-
{
528-
data: 1,
529-
isFetching: false,
530-
isSuccess: true,
531-
},
532-
])
562+
expect(states.length).toBe(3)
563+
expect(states[0]).toMatchObject({
564+
data: undefined,
565+
isFetching: false,
566+
isSuccess: false,
567+
isStale: true,
568+
})
569+
expect(states[1]).toMatchObject({
570+
data: undefined,
571+
isFetching: true,
572+
isSuccess: false,
573+
isStale: true,
574+
})
575+
expect(states[2]).toMatchObject({
576+
data: 1,
577+
isFetching: false,
578+
isSuccess: true,
579+
isStale: true,
580+
})
533581
})
534582

535583
it('should keep the previous data when keepPreviousData is set', async () => {
@@ -905,13 +953,11 @@ describe('useQuery', () => {
905953

906954
render(<Page />)
907955

908-
await waitFor(() =>
909-
expect(states).toMatchObject([
910-
{ isStale: true },
911-
{ isStale: false },
912-
{ isStale: true },
913-
])
914-
)
956+
await waitForMs(100)
957+
958+
expect(states[0]).toMatchObject({ isStale: true })
959+
expect(states[1]).toMatchObject({ isStale: false })
960+
expect(states[2]).toMatchObject({ isStale: true })
915961
})
916962

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

0 commit comments

Comments
 (0)