From 0b9c6ed9e0924112b5c2a7a0f3877f8ccaff7250 Mon Sep 17 00:00:00 2001 From: Niek Date: Fri, 11 Sep 2020 21:21:24 +0200 Subject: [PATCH] feat: cancel current fetch when fetching more --- src/core/query.ts | 49 ++++++--- src/core/utils.ts | 7 +- src/react/tests/useInfiniteQuery.test.tsx | 127 +++++++++++++++++++++- 3 files changed, 164 insertions(+), 19 deletions(-) diff --git a/src/core/query.ts b/src/core/query.ts index 54483ac29f..64da77d5d8 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -10,6 +10,7 @@ import { isOnline, isServer, isValidTimeout, + noop, replaceEqualDeep, sleep, } from './utils' @@ -108,7 +109,7 @@ export class Query { private queryCache: QueryCache private promise?: Promise private gcTimeout?: number - private cancelFetch?: () => void + private cancelFetch?: (silent?: boolean) => void private continueFetch?: () => void private isTransportCancelable?: boolean @@ -154,8 +155,12 @@ export class Query { }, this.cacheTime) } - cancel(): void { - this.cancelFetch?.() + async cancel(silent?: boolean): Promise { + const promise = this.promise + if (promise && this.cancelFetch) { + this.cancelFetch(silent) + await promise.catch(noop) + } } private continue(): void { @@ -311,9 +316,14 @@ export class Query { options?: FetchOptions, config?: ResolvedQueryConfig ): Promise { - // If we are already fetching, return current promise if (this.promise) { - return this.promise + if (options?.fetchMore && this.state.data) { + // Silently cancel current fetch if the user wants to fetch more + await this.cancel(true) + } else { + // Return current promise if we are already fetching + return this.promise + } } // Update config if passed, otherwise the config from the last execution is used @@ -346,11 +356,13 @@ export class Query { // Return data return data } catch (error) { - // Set error state - this.dispatch({ - type: ActionType.Error, - error, - }) + // Set error state if needed + if (!(isCancelledError(error) && error.silent)) { + this.dispatch({ + type: ActionType.Error, + error, + }) + } // Log error if (!isCancelledError(error)) { @@ -432,7 +444,10 @@ export class Query { } // Set to fetching state if not already in it - if (!this.state.isFetching) { + if ( + !this.state.isFetching || + this.state.isFetchingMore !== isFetchingMore + ) { this.dispatch({ type: ActionType.Fetch, isFetchingMore }) } @@ -471,11 +486,9 @@ export class Query { } // Create callback to cancel this fetch - this.cancelFetch = () => { - reject(new CancelledError()) - try { - cancelTransport?.() - } catch {} + this.cancelFetch = silent => { + reject(new CancelledError(silent)) + cancelTransport?.() } // Create callback to continue this fetch @@ -492,7 +505,9 @@ export class Query { // Check if the transport layer support cancellation if (isCancelable(promiseOrValue)) { cancelTransport = () => { - promiseOrValue.cancel() + try { + promiseOrValue.cancel() + } catch {} } this.isTransportCancelable = true } diff --git a/src/core/utils.ts b/src/core/utils.ts index a1a36a3e02..bd7ca2dde6 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -20,7 +20,12 @@ interface Cancelable { cancel(): void } -export class CancelledError {} +export class CancelledError { + silent?: boolean + constructor(silent?: boolean) { + this.silent = silent + } +} // UTILS diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx index 86deb29d08..fdf5a04b82 100644 --- a/src/react/tests/useInfiniteQuery.test.tsx +++ b/src/react/tests/useInfiniteQuery.test.tsx @@ -1,7 +1,7 @@ import { render, waitFor, fireEvent } from '@testing-library/react' import * as React from 'react' -import { sleep, queryKey } from './utils' +import { sleep, queryKey, waitForMs } from './utils' import { useInfiniteQuery, useQueryCache } from '..' import { InfiniteQueryResult } from '../../core' @@ -257,6 +257,131 @@ describe('useInfiniteQuery', () => { }) }) + it('should silently cancel any ongoing fetch when fetching more', async () => { + const key = queryKey() + const states: InfiniteQueryResult[] = [] + + function Page() { + const start = 10 + const state = useInfiniteQuery( + key, + async (_key, page: number = start) => { + await sleep(50) + return page + }, + { + getFetchMore: (lastPage, _pages) => lastPage + 1, + } + ) + + states.push(state) + + const { refetch, fetchMore } = state + + React.useEffect(() => { + setTimeout(() => { + refetch() + }, 100) + setTimeout(() => { + fetchMore() + }, 110) + }, [fetchMore, refetch]) + + return null + } + + render() + + await waitFor(() => expect(states.length).toBe(5)) + + expect(states[0]).toMatchObject({ + canFetchMore: undefined, + data: undefined, + isFetching: true, + isFetchingMore: false, + isSuccess: false, + }) + expect(states[1]).toMatchObject({ + canFetchMore: true, + data: [10], + isFetching: false, + isFetchingMore: false, + isSuccess: true, + }) + expect(states[2]).toMatchObject({ + canFetchMore: true, + data: [10], + isFetching: true, + isFetchingMore: false, + isSuccess: true, + }) + expect(states[3]).toMatchObject({ + canFetchMore: true, + data: [10], + isFetching: true, + isFetchingMore: 'next', + isSuccess: true, + }) + expect(states[4]).toMatchObject({ + canFetchMore: true, + data: [10, 11], + isFetching: false, + isFetchingMore: false, + isSuccess: true, + }) + }) + + it('should keep fetching first page when not loaded yet and triggering fetch more', async () => { + const key = queryKey() + const states: InfiniteQueryResult[] = [] + + function Page() { + const start = 10 + const state = useInfiniteQuery( + key, + async (_key, page: number = start) => { + await sleep(50) + return page + }, + { + getFetchMore: (lastPage, _pages) => lastPage + 1, + } + ) + + states.push(state) + + const { refetch, fetchMore } = state + + React.useEffect(() => { + setTimeout(() => { + fetchMore() + }, 10) + }, [fetchMore, refetch]) + + return null + } + + render() + + await waitForMs(100) + + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ + canFetchMore: undefined, + data: undefined, + isFetching: true, + isFetchingMore: false, + isSuccess: false, + }) + expect(states[1]).toMatchObject({ + canFetchMore: true, + data: [10], + isFetching: false, + isFetchingMore: false, + isSuccess: true, + }) + }) + it('should be able to override the cursor in the fetchMore callback', async () => { const key = queryKey() const states: InfiniteQueryResult[] = []