diff --git a/src/core/mutation.ts b/src/core/mutation.ts index cec82bee82..093f4107ee 100644 --- a/src/core/mutation.ts +++ b/src/core/mutation.ts @@ -131,11 +131,7 @@ export class Mutation< removeObserver(observer: MutationObserver): void { this.observers = this.observers.filter(x => x !== observer) - if (this.cacheTime) { - this.scheduleGc() - } else { - this.optionalRemove() - } + this.scheduleGc() this.mutationCache.notify({ type: 'observerRemoved', diff --git a/src/core/query.ts b/src/core/query.ts index 4d30855b43..ed449d8e56 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -154,6 +154,7 @@ export class Query< revertState?: QueryState state: QueryState meta: QueryMeta | undefined + isFetchingOptimistic?: boolean private cache: QueryCache private promise?: Promise @@ -161,13 +162,11 @@ export class Query< private observers: QueryObserver[] private defaultOptions?: QueryOptions private abortSignalConsumed: boolean - private hadObservers: boolean constructor(config: QueryConfig) { super() this.abortSignalConsumed = false - this.hadObservers = false this.defaultOptions = config.defaultOptions this.setOptions(config.options) this.observers = [] @@ -177,7 +176,6 @@ export class Query< this.initialState = config.state || this.getDefaultState(this.options) this.state = this.initialState this.meta = config.meta - this.scheduleGc() } private setOptions( @@ -197,12 +195,8 @@ export class Query< } protected optionalRemove() { - if (!this.observers.length) { - if (this.state.fetchStatus === 'idle') { - this.cache.remove(this) - } else if (this.hadObservers) { - this.scheduleGc() - } + if (!this.observers.length && this.state.fetchStatus === 'idle') { + this.cache.remove(this) } } @@ -307,7 +301,6 @@ export class Query< addObserver(observer: QueryObserver): void { if (this.observers.indexOf(observer) === -1) { this.observers.push(observer) - this.hadObservers = true // Stop the query from being garbage collected this.clearGcTimeout() @@ -331,11 +324,7 @@ export class Query< } } - if (this.cacheTime) { - this.scheduleGc() - } else { - this.cache.remove(this) - } + this.scheduleGc() } this.cache.notify({ type: 'observerRemoved', query: this, observer }) @@ -455,10 +444,11 @@ export class Query< // Notify cache callback this.cache.config.onSuccess?.(data, this as Query) - // Remove query after fetching if cache time is 0 - if (this.cacheTime === 0) { - this.optionalRemove() + if (!this.isFetchingOptimistic) { + // Schedule query gc after fetching + this.scheduleGc() } + this.isFetchingOptimistic = false }, onError: (error: TError | { silent?: boolean }) => { // Optimistically update state if needed @@ -477,10 +467,11 @@ export class Query< getLogger().error(error) } - // Remove query after fetching if cache time is 0 - if (this.cacheTime === 0) { - this.optionalRemove() + if (!this.isFetchingOptimistic) { + // Schedule query gc after fetching + this.scheduleGc() } + this.isFetchingOptimistic = false }, onFail: () => { this.dispatch({ type: 'failed' }) diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index 8e1a7b826c..6ed455d60f 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -309,15 +309,8 @@ export class QueryObserver< const query = this.client .getQueryCache() - .build( - this.client, - defaultedOptions as QueryOptions< - TQueryFnData, - TError, - TQueryData, - TQueryKey - > - ) + .build(this.client, defaultedOptions) + query.isFetchingOptimistic = true return query.fetch().then(() => this.createResult(query, defaultedOptions)) } @@ -655,17 +648,7 @@ export class QueryObserver< } private updateQuery(): void { - const query = this.client - .getQueryCache() - .build( - this.client, - this.options as QueryOptions< - TQueryFnData, - TError, - TQueryData, - TQueryKey - > - ) + const query = this.client.getQueryCache().build(this.client, this.options) if (query === this.currentQuery) { return diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx index 21d3fe2888..118ac6662b 100644 --- a/src/core/tests/query.test.tsx +++ b/src/core/tests/query.test.tsx @@ -13,6 +13,7 @@ import { onlineManager, QueryFunctionContext, } from '../..' +import { waitFor } from '@testing-library/react' describe('query', () => { let queryClient: QueryClient @@ -472,7 +473,6 @@ describe('query', () => { }) test('queries with cacheTime 0 should be removed immediately after unsubscribing', async () => { - const consoleMock = mockConsoleError() const key = queryKey() let count = 0 const observer = new QueryObserver(queryClient, { @@ -486,13 +486,12 @@ describe('query', () => { }) const unsubscribe1 = observer.subscribe() unsubscribe1() - await sleep(10) + await waitFor(() => expect(queryCache.find(key)).toBeUndefined()) const unsubscribe2 = observer.subscribe() unsubscribe2() - await sleep(10) - expect(count).toBe(2) - expect(queryCache.find(key)).toBeUndefined() - consoleMock.mockRestore() + + await waitFor(() => expect(queryCache.find(key)).toBeUndefined()) + expect(count).toBe(1) }) test('should be garbage collected when unsubscribed to', async () => { @@ -506,7 +505,7 @@ describe('query', () => { const unsubscribe = observer.subscribe() expect(queryCache.find(key)).toBeDefined() unsubscribe() - expect(queryCache.find(key)).toBeUndefined() + await waitFor(() => expect(queryCache.find(key)).toBeUndefined()) }) test('should be garbage collected later when unsubscribed and query is fetching', async () => { @@ -529,7 +528,7 @@ describe('query', () => { expect(queryCache.find(key)).toBeDefined() await sleep(10) // should be removed after an additional staleTime wait - expect(queryCache.find(key)).toBeUndefined() + await waitFor(() => expect(queryCache.find(key)).toBeUndefined()) }) test('should not be garbage collected unless there are no subscribers', async () => { diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx index 68cc4e5671..53de7d72ae 100644 --- a/src/core/tests/queryClient.test.tsx +++ b/src/core/tests/queryClient.test.tsx @@ -416,9 +416,10 @@ describe('queryClient', () => { }, { cacheTime: 0 } ) - const result2 = queryClient.getQueryData(key1) expect(result).toEqual(1) - expect(result2).toEqual(undefined) + await waitFor(() => + expect(queryClient.getQueryData(key1)).toEqual(undefined) + ) }) test('should keep a query in cache if cache time is Infinity', async () => { diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index b6e8a01614..b8b04fbc13 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -691,52 +691,78 @@ describe('useQuery', () => { expect(states[1]).toMatchObject({ data: 'data' }) }) - it('should create a new query when re-mounting with cacheTime 0', async () => { + it('should pick up a query when re-mounting with cacheTime 0', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const [toggle, setToggle] = React.useState(false) - React.useEffect(() => { - setActTimeout(() => { - setToggle(true) - }, 20) - }, [setToggle]) - - return toggle ? : + return ( +
+ + {toggle ? ( + + ) : ( + + )} +
+ ) } - function Component() { + function Component({ value }: { value: string }) { const state = useQuery( key, async () => { - await sleep(5) - return 'data' + await sleep(10) + return 'data: ' + value }, { cacheTime: 0, + notifyOnChangeProps: 'all', } ) states.push(state) - return null + return ( +
+
{state.data}
+
+ ) } - renderWithClient(queryClient, ) + const rendered = renderWithClient(queryClient, ) - await sleep(100) + await rendered.findByText('data: 1') - expect(states.length).toBe(5) + rendered.getByRole('button', { name: /toggle/i }).click() + + await rendered.findByText('data: 2') + + expect(states.length).toBe(4) // First load - expect(states[0]).toMatchObject({ isLoading: true, isSuccess: false }) + expect(states[0]).toMatchObject({ + isLoading: true, + isSuccess: false, + isFetching: true, + }) // First success - expect(states[1]).toMatchObject({ isLoading: false, isSuccess: true }) - // Switch - expect(states[2]).toMatchObject({ isLoading: false, isSuccess: true }) - // Second load - expect(states[3]).toMatchObject({ isLoading: true, isSuccess: false }) + expect(states[1]).toMatchObject({ + isLoading: false, + isSuccess: true, + isFetching: false, + }) + // Switch, goes to fetching + expect(states[2]).toMatchObject({ + isLoading: false, + isSuccess: true, + isFetching: true, + }) // Second success - expect(states[4]).toMatchObject({ isLoading: false, isSuccess: true }) + expect(states[3]).toMatchObject({ + isLoading: false, + isSuccess: true, + isFetching: false, + }) }) it('should not get into an infinite loop when removing a query with cacheTime 0 and rerendering', async () => { @@ -5097,64 +5123,6 @@ describe('useQuery', () => { onlineMock.mockRestore() }) - - it('online queries with cacheTime:0 should not fetch if paused and then unmounted', async () => { - const key = queryKey() - let count = 0 - - function Component() { - const state = useQuery({ - queryKey: key, - queryFn: async () => { - count++ - await sleep(10) - return 'data' + count - }, - cacheTime: 0, - }) - - return ( -
-
- status: {state.status}, fetchStatus: {state.fetchStatus} -
-
data: {state.data}
-
- ) - } - - function Page() { - const [show, setShow] = React.useState(true) - - return ( -
- {show && } - -
- ) - } - - const onlineMock = mockNavigatorOnLine(false) - - const rendered = renderWithClient(queryClient, ) - - await waitFor(() => - rendered.getByText('status: loading, fetchStatus: paused') - ) - - rendered.getByRole('button', { name: /hide/i }).click() - - onlineMock.mockReturnValue(true) - window.dispatchEvent(new Event('online')) - - await sleep(15) - - expect(queryClient.getQueryState(key)).not.toBeDefined() - - expect(count).toBe(0) - - onlineMock.mockRestore() - }) }) describe('networkMode always', () => { diff --git a/src/reactjs/useBaseQuery.ts b/src/reactjs/useBaseQuery.ts index 5d79aea354..a5f4d5f5dd 100644 --- a/src/reactjs/useBaseQuery.ts +++ b/src/reactjs/useBaseQuery.ts @@ -59,12 +59,6 @@ export function useBaseQuery< if (typeof defaultedOptions.staleTime !== 'number') { defaultedOptions.staleTime = 1000 } - - // Set cache time to 1 if the option has been set to 0 - // when using suspense to prevent infinite loop of fetches - if (defaultedOptions.cacheTime === 0) { - defaultedOptions.cacheTime = 1 - } } if (defaultedOptions.suspense || defaultedOptions.useErrorBoundary) {