Skip to content

Commit e892557

Browse files
authored
V4: streamline cancel refetch (#2937)
* feat: streamline cancelRefetch the following functions now default to true for cancelRefetch: - refetchQueries (+invalidateQueries, + resetQueries) - query.refetch - fetchNextPage (unchanged) - fetchPreviousPage (unchanged) * feat: streamline cancelRefetch make sure that refetchOnReconnect and refetchOnWindowFocus do not cancel already running requests * feat: streamline cancelRefetch update tests refetch and invalidate now both cancel previous queries, which is intended, so we get more calls to the queryFn in these cases * feat: streamline cancelRefetch add more tests for cancelRefetch behavior * feat: streamline cancelRefetch update docs and migration guide * feat: streamline cancelRefetch simplify conditions by moving the ?? true default down to fetch on observer level; all 3 callers (fetchNextPage, fetchPreviousPage and refetch) just pass their options down and adhere to this default; refetch also only has 3 callers: - refetch from useQuery, where we want the default - onOnline and onFocus, where we now explicitly pass false to keep the previous behavior and add more tests * feat: streamline cancelRefetch we always call this.fetch() with options, so we can just as well make the mandatory also, streamline signatures by destructing values that can't be forwarded (and use empty object as default value) in options and just spread the rest * feat: streamline cancelRefetch fix types for refetch it was accidentally made too wide and allowed all refetchFilters, like `predicate`; but with `refetch` on an obserserver, there is nothing to filter for, except the page, so that is what we need to accept via `RefetchPageFilters` * feat: streamline cancelRefetch refetch never took a queryKey as param - it is always bound to the observer
1 parent ad73004 commit e892557

File tree

10 files changed

+215
-36
lines changed

10 files changed

+215
-36
lines changed

docs/src/pages/guides/migrating-to-react-query-4.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ With version [3.22.0](https://github.com/tannerlinsley/react-query/releases/tag/
1414
+ import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query'
1515
```
1616

17+
### Consistent behavior for `cancelRefetch`
1718

19+
The `cancelRefetch` can be passed to all functions that imperatively fetch a query, namely:
1820

21+
- `queryClient.refetchQueries`
22+
- `queryClient.invalidateQueries`
23+
- `queryClient.resetQueries`
24+
- `refetch` returned from `useQuery`
25+
- `fetchNetPage` and `fetchPreviousPage` returned from `useInfiniteQuery`
1926

27+
Except for `fetchNetxPage` and `fetchPreviousPage`, this flag was defaulting to `false`, which was inconsistent and potentially troublesome: Calling `refetchQueries` or `invalidateQueries` after a mutation might not yield the latest result if a previous slow fetch was already ongoing, because this refetch would have been skipped.
28+
29+
We believe that if a query is actively refetched by some code you write, it should, per default, re-start the fetch.
30+
31+
That is why this flag now defaults to _true_ for all methods mentioned above. It also means that if you call `refetchQueries` twice in a row, without awaiting it, it will now cancel the first fetch and re-start it with the second one:
32+
33+
```
34+
queryClient.refetchQueries({ queryKey: ['todos'] })
35+
// this will abort the previous refetch and start a new fetch
36+
queryClient.refetchQueries({ queryKey: ['todos'] })
37+
```
38+
39+
You can opt-out of this behaviour by explicitly passing `cancelRefetch:false`:
40+
41+
```
42+
queryClient.refetchQueries({ queryKey: ['todos'] })
43+
// this will not abort the previous refetch - it will just be ignored
44+
queryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false })
45+
```
46+
47+
> Note: There is no change in behaviour for automatically triggered fetches, e.g. because a query mounts or because of a window focus refetch.

docs/src/pages/reference/QueryClient.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,10 @@ await queryClient.invalidateQueries('posts', {
295295
- `options?: InvalidateOptions`:
296296
- `throwOnError?: boolean`
297297
- When set to `true`, this method will throw if any of the query refetch tasks fail.
298-
- cancelRefetch?: boolean
299-
- When set to `true`, then the current request will be cancelled before a new request is made
298+
- `cancelRefetch?: boolean`
299+
- Defaults to `true`
300+
- Per default, a currently running request will be cancelled before a new request is made
301+
- When set to `false`, no refetch will be made if there is already a request running.
300302

301303
## `queryClient.refetchQueries`
302304

@@ -328,8 +330,10 @@ await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })
328330
- `options?: RefetchOptions`:
329331
- `throwOnError?: boolean`
330332
- When set to `true`, this method will throw if any of the query refetch tasks fail.
331-
- cancelRefetch?: boolean
332-
- When set to `true`, then the current request will be cancelled before a new request is made
333+
- `cancelRefetch?: boolean`
334+
- Defaults to `true`
335+
- Per default, a currently running request will be cancelled before a new request is made
336+
- When set to `false`, no refetch will be made if there is already a request running.
333337

334338
**Returns**
335339

@@ -396,8 +400,10 @@ queryClient.resetQueries(queryKey, { exact: true })
396400
- `options?: ResetOptions`:
397401
- `throwOnError?: boolean`
398402
- When set to `true`, this method will throw if any of the query refetch tasks fail.
399-
- cancelRefetch?: boolean
400-
- When set to `true`, then the current request will be cancelled before a new request is made
403+
- `cancelRefetch?: boolean`
404+
- Defaults to `true`
405+
- Per default, a currently running request will be cancelled before a new request is made
406+
- When set to `false`, no refetch will be made if there is already a request running.
401407

402408
**Returns**
403409

docs/src/pages/reference/useQuery.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ const result = useQuery({
236236
- `refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>`
237237
- A function to manually refetch the query.
238238
- If the query errors, the error will only be logged. If you want an error to be thrown, pass the `throwOnError: true` option
239-
- If `cancelRefetch` is `true`, then the current request will be cancelled before a new request is made
239+
- `cancelRefetch?: boolean`
240+
- Defaults to `true`
241+
- Per default, a currently running request will be cancelled before a new request is made
242+
- When set to `false`, no refetch will be made if there is already a request running.
240243
- `remove: () => void`
241244
- A function to remove the query from the cache.

src/core/infiniteQueryObserver.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class InfiniteQueryObserver<
3939

4040
// Type override
4141
protected fetch!: (
42-
fetchOptions?: ObserverFetchOptions
42+
fetchOptions: ObserverFetchOptions
4343
) => Promise<InfiniteQueryObserverResult<TData, TError>>
4444

4545
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
@@ -90,28 +90,27 @@ export class InfiniteQueryObserver<
9090
>
9191
}
9292

93-
fetchNextPage(
94-
options?: FetchNextPageOptions
95-
): Promise<InfiniteQueryObserverResult<TData, TError>> {
93+
fetchNextPage({ pageParam, ...options }: FetchNextPageOptions = {}): Promise<
94+
InfiniteQueryObserverResult<TData, TError>
95+
> {
9696
return this.fetch({
97-
// TODO consider removing `?? true` in future breaking change, to be consistent with `refetch` API (see https://github.com/tannerlinsley/react-query/issues/2617)
98-
cancelRefetch: options?.cancelRefetch ?? true,
99-
throwOnError: options?.throwOnError,
97+
...options,
10098
meta: {
101-
fetchMore: { direction: 'forward', pageParam: options?.pageParam },
99+
fetchMore: { direction: 'forward', pageParam },
102100
},
103101
})
104102
}
105103

106-
fetchPreviousPage(
107-
options?: FetchPreviousPageOptions
108-
): Promise<InfiniteQueryObserverResult<TData, TError>> {
104+
fetchPreviousPage({
105+
pageParam,
106+
...options
107+
}: FetchPreviousPageOptions = {}): Promise<
108+
InfiniteQueryObserverResult<TData, TError>
109+
> {
109110
return this.fetch({
110-
// TODO consider removing `?? true` in future breaking change, to be consistent with `refetch` API (see https://github.com/tannerlinsley/react-query/issues/2617)
111-
cancelRefetch: options?.cancelRefetch ?? true,
112-
throwOnError: options?.throwOnError,
111+
...options,
113112
meta: {
114-
fetchMore: { direction: 'backward', pageParam: options?.pageParam },
113+
fetchMore: { direction: 'backward', pageParam },
115114
},
116115
})
117116
}

src/core/query.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ export class Query<
297297
const observer = this.observers.find(x => x.shouldFetchOnWindowFocus())
298298

299299
if (observer) {
300-
observer.refetch()
300+
observer.refetch({ cancelRefetch: false })
301301
}
302302

303303
// Continue fetch if currently paused
@@ -308,7 +308,7 @@ export class Query<
308308
const observer = this.observers.find(x => x.shouldFetchOnReconnect())
309309

310310
if (observer) {
311-
observer.refetch()
311+
observer.refetch({ cancelRefetch: false })
312312
}
313313

314314
// Continue fetch if currently paused

src/core/queryClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export class QueryClient {
291291
this.queryCache.findAll(filters).map(query =>
292292
query.fetch(undefined, {
293293
...options,
294+
cancelRefetch: options?.cancelRefetch ?? true,
294295
meta: { refetchPage: filters?.refetchPage },
295296
})
296297
)

src/core/queryObserver.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RefetchQueryFilters } from './types'
1+
import { RefetchPageFilters } from './types'
22
import {
33
isServer,
44
isValidTimeout,
@@ -278,12 +278,15 @@ export class QueryObserver<
278278
this.client.getQueryCache().remove(this.currentQuery)
279279
}
280280

281-
refetch<TPageData>(
282-
options?: RefetchOptions & RefetchQueryFilters<TPageData>
283-
): Promise<QueryObserverResult<TData, TError>> {
281+
refetch<TPageData>({
282+
refetchPage,
283+
...options
284+
}: RefetchOptions & RefetchPageFilters<TPageData> = {}): Promise<
285+
QueryObserverResult<TData, TError>
286+
> {
284287
return this.fetch({
285288
...options,
286-
meta: { refetchPage: options?.refetchPage },
289+
meta: { refetchPage },
287290
})
288291
}
289292

@@ -314,9 +317,12 @@ export class QueryObserver<
314317
}
315318

316319
protected fetch(
317-
fetchOptions?: ObserverFetchOptions
320+
fetchOptions: ObserverFetchOptions
318321
): Promise<QueryObserverResult<TData, TError>> {
319-
return this.executeFetch(fetchOptions).then(() => {
322+
return this.executeFetch({
323+
...fetchOptions,
324+
cancelRefetch: fetchOptions.cancelRefetch ?? true,
325+
}).then(() => {
320326
this.updateResult()
321327
return this.currentResult
322328
})

src/core/tests/queryClient.test.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,8 @@ describe('queryClient', () => {
694694
queryClient.invalidateQueries(key1)
695695
await queryClient.refetchQueries({ stale: true })
696696
unsubscribe()
697-
expect(queryFn1).toHaveBeenCalledTimes(2)
697+
// fetchQuery, observer mount, invalidation (cancels observer mount) and refetch
698+
expect(queryFn1).toHaveBeenCalledTimes(4)
698699
expect(queryFn2).toHaveBeenCalledTimes(1)
699700
})
700701

@@ -711,7 +712,10 @@ describe('queryClient', () => {
711712
queryFn: queryFn1,
712713
})
713714
const unsubscribe = observer.subscribe()
714-
await queryClient.refetchQueries({ active: true, stale: true })
715+
await queryClient.refetchQueries(
716+
{ active: true, stale: true },
717+
{ cancelRefetch: false }
718+
)
715719
unsubscribe()
716720
expect(queryFn1).toHaveBeenCalledTimes(2)
717721
expect(queryFn2).toHaveBeenCalledTimes(1)
@@ -940,9 +944,10 @@ describe('queryClient', () => {
940944
expect(queryFn2).toHaveBeenCalledTimes(1)
941945
})
942946

943-
test('should cancel ongoing fetches if cancelRefetch option is passed', async () => {
947+
test('should cancel ongoing fetches if cancelRefetch option is set (default value)', async () => {
944948
const key = queryKey()
945949
const cancelFn = jest.fn()
950+
let fetchCount = 0
946951
const observer = new QueryObserver(queryClient, {
947952
queryKey: key,
948953
enabled: false,
@@ -952,16 +957,45 @@ describe('queryClient', () => {
952957

953958
queryClient.fetchQuery(key, () => {
954959
const promise = new Promise(resolve => {
960+
fetchCount++
955961
setTimeout(() => resolve(5), 10)
956962
})
957963
// @ts-expect-error
958964
promise.cancel = cancelFn
959965
return promise
960966
})
961967

962-
await queryClient.refetchQueries(undefined, { cancelRefetch: true })
968+
await queryClient.refetchQueries()
963969
observer.destroy()
964970
expect(cancelFn).toHaveBeenCalledTimes(1)
971+
expect(fetchCount).toBe(2)
972+
})
973+
974+
test('should not cancel ongoing fetches if cancelRefetch option is set to false', async () => {
975+
const key = queryKey()
976+
const cancelFn = jest.fn()
977+
let fetchCount = 0
978+
const observer = new QueryObserver(queryClient, {
979+
queryKey: key,
980+
enabled: false,
981+
initialData: 1,
982+
})
983+
observer.subscribe()
984+
985+
queryClient.fetchQuery(key, () => {
986+
const promise = new Promise(resolve => {
987+
fetchCount++
988+
setTimeout(() => resolve(5), 10)
989+
})
990+
// @ts-expect-error
991+
promise.cancel = cancelFn
992+
return promise
993+
})
994+
995+
await queryClient.refetchQueries(undefined, { cancelRefetch: false })
996+
observer.destroy()
997+
expect(cancelFn).toHaveBeenCalledTimes(0)
998+
expect(fetchCount).toBe(1)
965999
})
9661000
})
9671001

src/core/tests/queryObserver.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ describe('queryObserver', () => {
622622
select: () => selectedData,
623623
})
624624

625-
await observer.refetch({ queryKey: key })
625+
await observer.refetch()
626626
expect(observer.getCurrentResult().data).toBe(selectedData)
627627

628628
unsubscribe()

0 commit comments

Comments
 (0)