diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md index 5bad26f35c..81f3b12067 100644 --- a/docs/src/pages/docs/api.md +++ b/docs/src/pages/docs/api.md @@ -83,7 +83,7 @@ const queryInfo = useQuery({ - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff. - A function like `attempt => attempt * 1000` applies linear backoff. - `staleTime: Int | Infinity` - - The time in milliseconds that cache data remains fresh. After a successful cache update, that cache data will become stale after this duration. + - The time in milliseconds after data is considered stale. - If set to `Infinity`, query will never go stale - `cacheTime: Int | Infinity` - The time in milliseconds that unused/inactive cache data remains in memory. When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration. diff --git a/src/core/query.ts b/src/core/query.ts index 1790ba06d3..14d076b4bd 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -591,11 +591,11 @@ function getDefaultState( return { ...getStatusProps(initialStatus), error: null, - isFetched: false, + isFetched: Boolean(config.initialFetched), isFetching: initialStatus === QueryStatus.Loading, isFetchingMore: false, failureCount: 0, - fetchedCount: 0, + fetchedCount: config.initialFetched ? 1 : 0, data: initialData, updatedAt: Date.now(), canFetchMore: hasMorePages(config, initialData), diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts index bc5a1cdbd3..96dfe5d124 100644 --- a/src/core/queryCache.ts +++ b/src/core/queryCache.ts @@ -339,7 +339,7 @@ export class QueryCache { } this.buildQuery(queryKey, { - initialStale: typeof config?.staleTime === 'undefined', + initialFetched: true, initialData: functionalUpdate(updater, undefined), ...config, }) diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index fc38b31601..74c88dcfa1 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -171,7 +171,7 @@ export class QueryObserver { } const timeElapsed = Date.now() - updatedAt - const timeUntilStale = staleTime - timeElapsed + const timeUntilStale = staleTime - timeElapsed + 1 const timeout = Math.max(timeUntilStale, 0) this.staleTimeoutId = setTimeout(() => { @@ -244,7 +244,7 @@ export class QueryObserver { isPreviousData = true } - let isStale = false + 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. diff --git a/src/core/types.ts b/src/core/types.ts index 9e363e8bb4..06339cc888 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -42,7 +42,6 @@ export interface BaseQueryConfig { */ retry?: boolean | number | ((failureCount: number, error: TError) => boolean) retryDelay?: number | ((retryAttempt: number) => number) - staleTime?: number cacheTime?: number isDataEqual?: (oldData: unknown, newData: unknown) => boolean queryFn?: QueryFunction @@ -50,7 +49,7 @@ export interface BaseQueryConfig { queryKeySerializerFn?: QueryKeySerializerFunction queryFnParamsFilter?: (args: ArrayQueryKey) => ArrayQueryKey initialData?: TResult | InitialDataFunction - initialStale?: boolean | InitialStaleFunction + initialFetched?: boolean infinite?: true /** * Set this to `false` to disable structural sharing between query results. @@ -75,6 +74,17 @@ export interface QueryObserverConfig< * Defaults to `true`. */ enabled?: boolean | unknown + /** + * The time in milliseconds after data is considered stale. + * If set to `Infinity`, the data will never be stale. + */ + staleTime?: number + /** + * If set, this will mark any `initialData` provided as stale and will likely cause it to be refetched on mount. + * If a function is passed, it will be called only when appropriate to resolve the `initialStale` value. + * This can be useful if your `initialStale` value is costly to calculate. + */ + initialStale?: boolean | InitialStaleFunction /** * If set to a number, the query will continuously refetch at this frequency in milliseconds. * Defaults to `false`. diff --git a/src/hydration/hydration.ts b/src/hydration/hydration.ts index 64b424230d..8a40cd890e 100644 --- a/src/hydration/hydration.ts +++ b/src/hydration/hydration.ts @@ -83,10 +83,8 @@ export function hydrate( for (const dehydratedQuery of queries) { const queryKey = dehydratedQuery.config.queryKey - const queryConfig: QueryConfig = dehydratedQuery.config as QueryConfig< - TResult - > - + const queryConfig = dehydratedQuery.config as QueryConfig + queryConfig.initialFetched = true const query = queryCache.buildQuery(queryKey, queryConfig) query.state.updatedAt = dehydratedQuery.updatedAt } diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx index 7395ff7be8..ff091c2fc3 100644 --- a/src/react/tests/useQuery.test.tsx +++ b/src/react/tests/useQuery.test.tsx @@ -1225,6 +1225,41 @@ describe('useQuery', () => { consoleMock.mockRestore() }) + it('should fetch on mount when a query was already created with setQueryData', async () => { + const key = queryKey() + const states: QueryResult[] = [] + + queryCache.setQueryData(key, 'prefetched') + + function Page() { + const state = useQuery(key, () => 'data') + states.push(state) + return null + } + + render() + + await waitFor(() => + expect(states).toMatchObject([ + { + data: 'prefetched', + isFetching: false, + isStale: true, + }, + { + data: 'prefetched', + isFetching: true, + isStale: true, + }, + { + data: 'data', + isFetching: false, + isStale: true, + }, + ]) + ) + }) + it('should refetch after focus regain', async () => { const key = queryKey() const states: QueryResult[] = [] @@ -1245,7 +1280,7 @@ describe('useQuery', () => { render() - await waitFor(() => expect(states.length).toBe(2)) + await waitFor(() => expect(states.length).toBe(3)) act(() => { // reset visibilityState to original value @@ -1253,21 +1288,26 @@ describe('useQuery', () => { window.dispatchEvent(new FocusEvent('focus')) }) - await waitFor(() => expect(states.length).toBe(4)) + await waitFor(() => expect(states.length).toBe(5)) expect(states).toMatchObject([ { data: 'prefetched', isFetching: false, - isStale: false, + isStale: true, }, { data: 'prefetched', + isFetching: true, + isStale: true, + }, + { + data: 'data', isFetching: false, isStale: true, }, { - data: 'prefetched', + data: 'data', isFetching: true, isStale: true, },