Skip to content

Commit 96a743e

Browse files
authoredJun 11, 2024··
feat(query-core): staleTime as a function (#7541)
* feat: staleTime as a function * test: staleTime as a function * refactor: types * docs: staleTime as function
1 parent ef72cd5 commit 96a743e

File tree

7 files changed

+76
-17
lines changed

7 files changed

+76
-17
lines changed
 

‎docs/framework/react/reference/useQuery.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ const {
8888
- This function receives a `retryAttempt` integer and the actual Error and returns the delay to apply before the next attempt in milliseconds.
8989
- A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff.
9090
- A function like `attempt => attempt * 1000` applies linear backoff.
91-
- `staleTime: number | Infinity`
91+
- `staleTime: number | ((query: Query) => number)`
9292
- Optional
9393
- Defaults to `0`
9494
- The time in milliseconds after data is considered stale. This value only applies to the hook it is defined on.
9595
- If set to `Infinity`, the data will never be considered stale
96+
- If set to a function, the function will be executed with the query to compute a `staleTime`.
9697
- `gcTime: number | Infinity`
9798
- Defaults to `5 * 60 * 1000` (5 minutes) or `Infinity` during SSR
9899
- 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. When different garbage collection times are specified, the longest one will be used.

‎packages/query-core/src/__tests__/queryObserver.test.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -910,4 +910,30 @@ describe('queryObserver', () => {
910910
const result = observer.getCurrentResult()
911911
expect(result.isStale).toBe(false)
912912
})
913+
914+
test('should allow staleTime as a function', async () => {
915+
const key = queryKey()
916+
const observer = new QueryObserver(queryClient, {
917+
queryKey: key,
918+
queryFn: async () => {
919+
await sleep(5)
920+
return {
921+
data: 'data',
922+
staleTime: 20,
923+
}
924+
},
925+
staleTime: (query) => query.state.data?.staleTime ?? 0,
926+
})
927+
const results: Array<QueryObserverResult<unknown>> = []
928+
const unsubscribe = observer.subscribe((x) => {
929+
if (x.data) {
930+
results.push(x)
931+
}
932+
})
933+
934+
await waitFor(() => expect(results[0]?.isStale).toBe(false))
935+
await waitFor(() => expect(results[1]?.isStale).toBe(true))
936+
937+
unsubscribe()
938+
})
913939
})

‎packages/query-core/src/queryCache.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,12 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
9797
this.#queries = new Map<string, Query>()
9898
}
9999

100-
build<TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
100+
build<
101+
TQueryFnData = unknown,
102+
TError = DefaultError,
103+
TData = TQueryFnData,
104+
TQueryKey extends QueryKey = QueryKey,
105+
>(
101106
client: QueryClient,
102107
options: WithRequired<
103108
QueryOptions<TQueryFnData, TError, TData, TQueryKey>,

‎packages/query-core/src/queryClient.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
hashQueryKeyByOptions,
55
noop,
66
partialMatchKey,
7+
resolveStaleTime,
78
skipToken,
89
} from './utils'
910
import { QueryCache } from './queryCache'
@@ -142,7 +143,7 @@ export class QueryClient {
142143

143144
if (
144145
options.revalidateIfStale &&
145-
query.isStaleByTime(defaultedOptions.staleTime)
146+
query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query))
146147
) {
147148
void this.prefetchQuery(defaultedOptions)
148149
}
@@ -343,7 +344,9 @@ export class QueryClient {
343344

344345
const query = this.#queryCache.build(this, defaultedOptions)
345346

346-
return query.isStaleByTime(defaultedOptions.staleTime)
347+
return query.isStaleByTime(
348+
resolveStaleTime(defaultedOptions.staleTime, query),
349+
)
347350
? query.fetch(defaultedOptions)
348351
: Promise.resolve(query.state.data as TData)
349352
}

‎packages/query-core/src/queryObserver.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
isValidTimeout,
44
noop,
55
replaceData,
6+
resolveStaleTime,
67
shallowEqualObjects,
78
timeUntilStale,
89
} from './utils'
@@ -190,7 +191,8 @@ export class QueryObserver<
190191
mounted &&
191192
(this.#currentQuery !== prevQuery ||
192193
this.options.enabled !== prevOptions.enabled ||
193-
this.options.staleTime !== prevOptions.staleTime)
194+
resolveStaleTime(this.options.staleTime, this.#currentQuery) !==
195+
resolveStaleTime(prevOptions.staleTime, this.#currentQuery))
194196
) {
195197
this.#updateStaleTimeout()
196198
}
@@ -338,19 +340,16 @@ export class QueryObserver<
338340

339341
#updateStaleTimeout(): void {
340342
this.#clearStaleTimeout()
343+
const staleTime = resolveStaleTime(
344+
this.options.staleTime,
345+
this.#currentQuery,
346+
)
341347

342-
if (
343-
isServer ||
344-
this.#currentResult.isStale ||
345-
!isValidTimeout(this.options.staleTime)
346-
) {
348+
if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) {
347349
return
348350
}
349351

350-
const time = timeUntilStale(
351-
this.#currentResult.dataUpdatedAt,
352-
this.options.staleTime,
353-
)
352+
const time = timeUntilStale(this.#currentResult.dataUpdatedAt, staleTime)
354353

355354
// The timeout is sometimes triggered 1 ms before the stale time expiration.
356355
// To mitigate this issue we always add 1 ms to the timeout.
@@ -742,7 +741,10 @@ function isStale(
742741
query: Query<any, any, any, any>,
743742
options: QueryObserverOptions<any, any, any, any, any>,
744743
): boolean {
745-
return options.enabled !== false && query.isStaleByTime(options.staleTime)
744+
return (
745+
options.enabled !== false &&
746+
query.isStaleByTime(resolveStaleTime(options.staleTime, query))
747+
)
746748
}
747749

748750
// this function would decide if we will update the observer's 'current'

‎packages/query-core/src/types.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ export type QueryFunction<
4747
TPageParam = never,
4848
> = (context: QueryFunctionContext<TQueryKey, TPageParam>) => T | Promise<T>
4949

50+
export type StaleTime<
51+
TQueryFnData = unknown,
52+
TError = DefaultError,
53+
TData = TQueryFnData,
54+
TQueryKey extends QueryKey = QueryKey,
55+
> = number | ((query: Query<TQueryFnData, TError, TData, TQueryKey>) => number)
56+
5057
export type QueryPersister<
5158
T = unknown,
5259
TQueryKey extends QueryKey = QueryKey,
@@ -254,8 +261,9 @@ export interface QueryObserverOptions<
254261
/**
255262
* The time in milliseconds after data is considered stale.
256263
* If set to `Infinity`, the data will never be considered stale.
264+
* If set to a function, the function will be executed with the query to compute a `staleTime`.
257265
*/
258-
staleTime?: number
266+
staleTime?: StaleTime<TQueryFnData, TError, TQueryData, TQueryKey>
259267
/**
260268
* If set to a number, the query will continuously refetch at this frequency in milliseconds.
261269
* If set to a function, the function will be executed with the latest data and query to compute a frequency
@@ -427,7 +435,7 @@ export interface FetchQueryOptions<
427435
* The time in milliseconds after data is considered stale.
428436
* If the data is fresh it will be returned from the cache.
429437
*/
430-
staleTime?: number
438+
staleTime?: StaleTime<TQueryFnData, TError, TData, TQueryKey>
431439
}
432440

433441
export interface EnsureQueryDataOptions<

‎packages/query-core/src/utils.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type {
2+
DefaultError,
23
FetchStatus,
34
MutationKey,
45
MutationStatus,
56
QueryFunction,
67
QueryKey,
78
QueryOptions,
9+
StaleTime,
810
} from './types'
911
import type { Mutation } from './mutation'
1012
import type { FetchOptions, Query } from './query'
@@ -86,6 +88,18 @@ export function timeUntilStale(updatedAt: number, staleTime?: number): number {
8688
return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0)
8789
}
8890

91+
export function resolveStaleTime<
92+
TQueryFnData = unknown,
93+
TError = DefaultError,
94+
TData = TQueryFnData,
95+
TQueryKey extends QueryKey = QueryKey,
96+
>(
97+
staleTime: undefined | StaleTime<TQueryFnData, TError, TData, TQueryKey>,
98+
query: Query<TQueryFnData, TError, TData, TQueryKey>,
99+
): number | undefined {
100+
return typeof staleTime === 'function' ? staleTime(query) : staleTime
101+
}
102+
89103
export function matchQuery(
90104
filters: QueryFilters,
91105
query: Query<any, any, any, any>,

0 commit comments

Comments
 (0)
Please sign in to comment.