Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 32 additions & 17 deletions src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isOnline,
isServer,
isValidTimeout,
noop,
replaceEqualDeep,
sleep,
} from './utils'
Expand Down Expand Up @@ -108,7 +109,7 @@ export class Query<TResult, TError> {
private queryCache: QueryCache
private promise?: Promise<TResult | undefined>
private gcTimeout?: number
private cancelFetch?: () => void
private cancelFetch?: (silent?: boolean) => void
private continueFetch?: () => void
private isTransportCancelable?: boolean

Expand Down Expand Up @@ -154,8 +155,12 @@ export class Query<TResult, TError> {
}, this.cacheTime)
}

cancel(): void {
this.cancelFetch?.()
async cancel(silent?: boolean): Promise<void> {
const promise = this.promise
if (promise && this.cancelFetch) {
this.cancelFetch(silent)
await promise.catch(noop)
}
}

private continue(): void {
Expand Down Expand Up @@ -311,9 +316,14 @@ export class Query<TResult, TError> {
options?: FetchOptions,
config?: ResolvedQueryConfig<TResult, TError>
): Promise<TResult | undefined> {
// 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
Expand Down Expand Up @@ -346,11 +356,13 @@ export class Query<TResult, TError> {
// 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)) {
Expand Down Expand Up @@ -432,7 +444,10 @@ export class Query<TResult, TError> {
}

// 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 })
}

Expand Down Expand Up @@ -471,11 +486,9 @@ export class Query<TResult, TError> {
}

// 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
Expand All @@ -492,7 +505,9 @@ export class Query<TResult, TError> {
// Check if the transport layer support cancellation
if (isCancelable(promiseOrValue)) {
cancelTransport = () => {
promiseOrValue.cancel()
try {
promiseOrValue.cancel()
} catch {}
}
this.isTransportCancelable = true
}
Expand Down
7 changes: 6 additions & 1 deletion src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ interface Cancelable {
cancel(): void
}

export class CancelledError {}
export class CancelledError {
silent?: boolean
constructor(silent?: boolean) {
this.silent = silent
}
}

// UTILS

Expand Down
127 changes: 126 additions & 1 deletion src/react/tests/useInfiniteQuery.test.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -257,6 +257,131 @@ describe('useInfiniteQuery', () => {
})
})

it('should silently cancel any ongoing fetch when fetching more', async () => {
const key = queryKey()
const states: InfiniteQueryResult<number>[] = []

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(<Page />)

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<number>[] = []

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(<Page />)

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<number>[] = []
Expand Down