Skip to content

Commit 1eb4d00

Browse files
authored
fix: cancel current fetch when fetching more (#1000)
1 parent 5c79f72 commit 1eb4d00

File tree

3 files changed

+164
-19
lines changed

3 files changed

+164
-19
lines changed

src/core/query.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
isOnline,
1111
isServer,
1212
isValidTimeout,
13+
noop,
1314
replaceEqualDeep,
1415
sleep,
1516
} from './utils'
@@ -108,7 +109,7 @@ export class Query<TResult, TError> {
108109
private queryCache: QueryCache
109110
private promise?: Promise<TResult | undefined>
110111
private gcTimeout?: number
111-
private cancelFetch?: () => void
112+
private cancelFetch?: (silent?: boolean) => void
112113
private continueFetch?: () => void
113114
private isTransportCancelable?: boolean
114115

@@ -154,8 +155,12 @@ export class Query<TResult, TError> {
154155
}, this.cacheTime)
155156
}
156157

157-
cancel(): void {
158-
this.cancelFetch?.()
158+
async cancel(silent?: boolean): Promise<void> {
159+
const promise = this.promise
160+
if (promise && this.cancelFetch) {
161+
this.cancelFetch(silent)
162+
await promise.catch(noop)
163+
}
159164
}
160165

161166
private continue(): void {
@@ -311,9 +316,14 @@ export class Query<TResult, TError> {
311316
options?: FetchOptions,
312317
config?: ResolvedQueryConfig<TResult, TError>
313318
): Promise<TResult | undefined> {
314-
// If we are already fetching, return current promise
315319
if (this.promise) {
316-
return this.promise
320+
if (options?.fetchMore && this.state.data) {
321+
// Silently cancel current fetch if the user wants to fetch more
322+
await this.cancel(true)
323+
} else {
324+
// Return current promise if we are already fetching
325+
return this.promise
326+
}
317327
}
318328

319329
// Update config if passed, otherwise the config from the last execution is used
@@ -346,11 +356,13 @@ export class Query<TResult, TError> {
346356
// Return data
347357
return data
348358
} catch (error) {
349-
// Set error state
350-
this.dispatch({
351-
type: ActionType.Error,
352-
error,
353-
})
359+
// Set error state if needed
360+
if (!(isCancelledError(error) && error.silent)) {
361+
this.dispatch({
362+
type: ActionType.Error,
363+
error,
364+
})
365+
}
354366

355367
// Log error
356368
if (!isCancelledError(error)) {
@@ -432,7 +444,10 @@ export class Query<TResult, TError> {
432444
}
433445

434446
// Set to fetching state if not already in it
435-
if (!this.state.isFetching) {
447+
if (
448+
!this.state.isFetching ||
449+
this.state.isFetchingMore !== isFetchingMore
450+
) {
436451
this.dispatch({ type: ActionType.Fetch, isFetchingMore })
437452
}
438453

@@ -471,11 +486,9 @@ export class Query<TResult, TError> {
471486
}
472487

473488
// Create callback to cancel this fetch
474-
this.cancelFetch = () => {
475-
reject(new CancelledError())
476-
try {
477-
cancelTransport?.()
478-
} catch {}
489+
this.cancelFetch = silent => {
490+
reject(new CancelledError(silent))
491+
cancelTransport?.()
479492
}
480493

481494
// Create callback to continue this fetch
@@ -492,7 +505,9 @@ export class Query<TResult, TError> {
492505
// Check if the transport layer support cancellation
493506
if (isCancelable(promiseOrValue)) {
494507
cancelTransport = () => {
495-
promiseOrValue.cancel()
508+
try {
509+
promiseOrValue.cancel()
510+
} catch {}
496511
}
497512
this.isTransportCancelable = true
498513
}

src/core/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ interface Cancelable {
2020
cancel(): void
2121
}
2222

23-
export class CancelledError {}
23+
export class CancelledError {
24+
silent?: boolean
25+
constructor(silent?: boolean) {
26+
this.silent = silent
27+
}
28+
}
2429

2530
// UTILS
2631

src/react/tests/useInfiniteQuery.test.tsx

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render, waitFor, fireEvent } from '@testing-library/react'
22
import * as React from 'react'
33

4-
import { sleep, queryKey } from './utils'
4+
import { sleep, queryKey, waitForMs } from './utils'
55
import { useInfiniteQuery, useQueryCache } from '..'
66
import { InfiniteQueryResult } from '../../core'
77

@@ -257,6 +257,131 @@ describe('useInfiniteQuery', () => {
257257
})
258258
})
259259

260+
it('should silently cancel any ongoing fetch when fetching more', async () => {
261+
const key = queryKey()
262+
const states: InfiniteQueryResult<number>[] = []
263+
264+
function Page() {
265+
const start = 10
266+
const state = useInfiniteQuery(
267+
key,
268+
async (_key, page: number = start) => {
269+
await sleep(50)
270+
return page
271+
},
272+
{
273+
getFetchMore: (lastPage, _pages) => lastPage + 1,
274+
}
275+
)
276+
277+
states.push(state)
278+
279+
const { refetch, fetchMore } = state
280+
281+
React.useEffect(() => {
282+
setTimeout(() => {
283+
refetch()
284+
}, 100)
285+
setTimeout(() => {
286+
fetchMore()
287+
}, 110)
288+
}, [fetchMore, refetch])
289+
290+
return null
291+
}
292+
293+
render(<Page />)
294+
295+
await waitFor(() => expect(states.length).toBe(5))
296+
297+
expect(states[0]).toMatchObject({
298+
canFetchMore: undefined,
299+
data: undefined,
300+
isFetching: true,
301+
isFetchingMore: false,
302+
isSuccess: false,
303+
})
304+
expect(states[1]).toMatchObject({
305+
canFetchMore: true,
306+
data: [10],
307+
isFetching: false,
308+
isFetchingMore: false,
309+
isSuccess: true,
310+
})
311+
expect(states[2]).toMatchObject({
312+
canFetchMore: true,
313+
data: [10],
314+
isFetching: true,
315+
isFetchingMore: false,
316+
isSuccess: true,
317+
})
318+
expect(states[3]).toMatchObject({
319+
canFetchMore: true,
320+
data: [10],
321+
isFetching: true,
322+
isFetchingMore: 'next',
323+
isSuccess: true,
324+
})
325+
expect(states[4]).toMatchObject({
326+
canFetchMore: true,
327+
data: [10, 11],
328+
isFetching: false,
329+
isFetchingMore: false,
330+
isSuccess: true,
331+
})
332+
})
333+
334+
it('should keep fetching first page when not loaded yet and triggering fetch more', async () => {
335+
const key = queryKey()
336+
const states: InfiniteQueryResult<number>[] = []
337+
338+
function Page() {
339+
const start = 10
340+
const state = useInfiniteQuery(
341+
key,
342+
async (_key, page: number = start) => {
343+
await sleep(50)
344+
return page
345+
},
346+
{
347+
getFetchMore: (lastPage, _pages) => lastPage + 1,
348+
}
349+
)
350+
351+
states.push(state)
352+
353+
const { refetch, fetchMore } = state
354+
355+
React.useEffect(() => {
356+
setTimeout(() => {
357+
fetchMore()
358+
}, 10)
359+
}, [fetchMore, refetch])
360+
361+
return null
362+
}
363+
364+
render(<Page />)
365+
366+
await waitForMs(100)
367+
368+
expect(states.length).toBe(2)
369+
expect(states[0]).toMatchObject({
370+
canFetchMore: undefined,
371+
data: undefined,
372+
isFetching: true,
373+
isFetchingMore: false,
374+
isSuccess: false,
375+
})
376+
expect(states[1]).toMatchObject({
377+
canFetchMore: true,
378+
data: [10],
379+
isFetching: false,
380+
isFetchingMore: false,
381+
isSuccess: true,
382+
})
383+
})
384+
260385
it('should be able to override the cursor in the fetchMore callback', async () => {
261386
const key = queryKey()
262387
const states: InfiniteQueryResult<number>[] = []

0 commit comments

Comments
 (0)