diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index ff821a6967..d46c2a4ae9 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -5,7 +5,7 @@ import { isValidTimeout, noop, } from './utils' -import { QueryResult, ResolvedQueryConfig, QueryStatus } from './types' +import type { QueryResult, ResolvedQueryConfig } from './types' import type { Query, Action, FetchMoreOptions, RefetchOptions } from './query' export type UpdateListener = ( @@ -19,12 +19,14 @@ export class QueryObserver { private currentResult!: QueryResult private previousQueryResult?: QueryResult private listener?: UpdateListener + private isStale: boolean private initialUpdateCount: number private staleTimeoutId?: number private refetchIntervalId?: number constructor(config: ResolvedQueryConfig) { this.config = config + this.isStale = true this.initialUpdateCount = 0 // Bind exposed methods @@ -128,9 +130,9 @@ export class QueryObserver { private optionalFetch(): void { if ( - this.config.enabled && // Don't auto refetch if disabled + this.config.enabled && // Only fetch if enabled + this.isStale && // Only fetch if stale !(this.config.suspense && this.currentResult.isFetched) && // Don't refetch if in suspense mode and the data is already fetched - this.currentResult.isStale && // Only refetch if stale (this.config.refetchOnMount || this.currentQuery.observers.length === 1) ) { this.fetch() @@ -148,7 +150,7 @@ export class QueryObserver { this.clearStaleTimeout() - if (this.currentResult.isStale || !isValidTimeout(this.config.staleTime)) { + if (this.isStale || !isValidTimeout(this.config.staleTime)) { return } @@ -157,8 +159,9 @@ export class QueryObserver { const timeout = Math.max(timeUntilStale, 0) this.staleTimeoutId = setTimeout(() => { - if (!this.currentResult.isStale) { - this.currentResult = { ...this.currentResult, isStale: true } + if (!this.isStale) { + this.isStale = true + this.updateResult() this.notify() this.config.queryCache.notifyGlobalListeners(this.currentQuery) } @@ -208,7 +211,7 @@ export class QueryObserver { } private updateResult(): void { - const { currentQuery, currentResult, previousQueryResult, config } = this + const { currentQuery, previousQueryResult, config } = this const { state } = currentQuery let { data, status, updatedAt } = state let isPreviousData = false @@ -216,9 +219,8 @@ export class QueryObserver { // Keep previous data if needed if ( config.keepPreviousData && - (state.status === QueryStatus.Idle || - state.status === QueryStatus.Loading) && - previousQueryResult?.status === QueryStatus.Success + state.isInitialData && + previousQueryResult?.isSuccess ) { data = previousQueryResult.data updatedAt = previousQueryResult.updatedAt @@ -226,22 +228,6 @@ export class QueryObserver { isPreviousData = true } - let isStale - - // When the query has not been fetched yet and this is the initial render, - // determine the staleness based on the initialStale or existence of initial data. - if (!currentResult && state.isInitialData) { - if (typeof config.initialStale === 'function') { - isStale = config.initialStale() - } else if (typeof config.initialStale === 'boolean') { - isStale = config.initialStale - } else { - isStale = typeof state.data === 'undefined' - } - } else { - isStale = currentQuery.isStaleByTime(config.staleTime) - } - this.currentResult = { ...getStatusProps(status), canFetchMore: state.canFetchMore, @@ -256,22 +242,16 @@ export class QueryObserver { isFetchingMore: state.isFetchingMore, isInitialData: state.isInitialData, isPreviousData, - isStale, + isStale: this.isStale, refetch: this.refetch, updatedAt, } } private updateQuery(): void { + const config = this.config const prevQuery = this.currentQuery - // Remove the initial data when there is an existing query - // because this data should not be used for a new query - const config = - this.config.keepPreviousData && prevQuery - ? { ...this.config, initialData: undefined } - : this.config - let query = config.queryCache.getQueryByHash( config.queryHash ) @@ -287,6 +267,22 @@ export class QueryObserver { this.previousQueryResult = this.currentResult this.currentQuery = query this.initialUpdateCount = query.state.updateCount + + // Update stale state on query switch + if (query.state.isInitialData) { + if (config.keepPreviousData && prevQuery) { + this.isStale = true + } else if (typeof config.initialStale === 'function') { + this.isStale = config.initialStale() + } else if (typeof config.initialStale === 'boolean') { + this.isStale = config.initialStale + } else { + this.isStale = typeof query.state.data === 'undefined' + } + } else { + this.isStale = query.isStaleByTime(config.staleTime) + } + this.updateResult() if (this.listener) { @@ -296,16 +292,20 @@ export class QueryObserver { } onQueryUpdate(action: Action): void { + const { config } = this const { type } = action + // Update stale state on success or error + if (type === 2 || type === 3) { + this.isStale = this.currentQuery.isStaleByTime(config.staleTime) + } + // Store current result and get new result const prevResult = this.currentResult this.updateResult() + const currentResult = this.currentResult - const { currentResult, config } = this - - // We need to check the action because the state could have - // transitioned from success to success in case of `setQueryData`. + // Trigger callbacks and timers on success or error if (type === 2) { config.onSuccess?.(currentResult.data!) config.onSettled?.(currentResult.data!, null) diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx index 15dcc1d2af..3238399621 100644 --- a/src/react/tests/useQuery.test.tsx +++ b/src/react/tests/useQuery.test.tsx @@ -1198,12 +1198,11 @@ describe('useQuery', () => { render() - await waitFor(() => expect(states.length).toBe(2)) + await waitForMs(10) - expect(states).toMatchObject([ - { data: 'initial', isStale: false }, - { data: 'initial', isStale: true }, - ]) + expect(states.length).toBe(2) + expect(states[0]).toMatchObject({ data: 'initial', isStale: false }) + expect(states[1]).toMatchObject({ data: 'initial', isStale: true }) }) it('should fetch if initial data is set and initial stale is set to true', async () => { @@ -1230,6 +1229,47 @@ describe('useQuery', () => { ]) }) + it('should fetch if initial data is set and initial stale is set to true with stale time', async () => { + const key = queryKey() + const states: QueryResult[] = [] + + function Page() { + const state = useQuery(key, () => 'data', { + staleTime: 50, + initialData: 'initial', + initialStale: true, + }) + states.push(state) + return null + } + + render() + + await waitForMs(100) + + expect(states.length).toBe(4) + expect(states[0]).toMatchObject({ + data: 'initial', + isStale: true, + isFetching: false, + }) + expect(states[1]).toMatchObject({ + data: 'initial', + isStale: true, + isFetching: true, + }) + expect(states[2]).toMatchObject({ + data: 'data', + isStale: false, + isFetching: false, + }) + expect(states[3]).toMatchObject({ + data: 'data', + isStale: true, + isFetching: false, + }) + }) + it('should keep initial stale and initial data when the query key changes', async () => { const key = queryKey() const states: QueryResult<{ count: number }>[] = [] @@ -1251,15 +1291,13 @@ describe('useQuery', () => { render() - await waitFor(() => expect(states.length).toBe(5)) + await waitForMs(100) - expect(states).toMatchObject([ - { data: { count: 0 } }, - { data: { count: 0 } }, - { data: { count: 1 } }, - { data: { count: 1 } }, - { data: { count: 10 } }, - ]) + expect(states.length).toBe(4) + expect(states[0]).toMatchObject({ data: { count: 0 } }) + expect(states[1]).toMatchObject({ data: { count: 0 } }) + expect(states[2]).toMatchObject({ data: { count: 1 } }) + expect(states[3]).toMatchObject({ data: { count: 1 } }) }) it('should retry specified number of times', async () => {