Skip to content

Commit 0d28e0a

Browse files
committed
refactor(core): add generic utilities for resolving value-or-function patterns, replace specialized resolveStaleTime and resolveEnabled
This commit introduces `isFunctionVariant()` and `resolveValueOrFunction()` utilities to replace repetitive `typeof === 'function'` checks throughout the codebase, consolidating the common "value or function that computes value" pattern.
1 parent 9e30aaf commit 0d28e0a

File tree

5 files changed

+132
-79
lines changed

5 files changed

+132
-79
lines changed

packages/query-core/src/query.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
ensureQueryFn,
33
noop,
44
replaceData,
5-
resolveEnabled,
5+
resolveValueOrFunction,
66
skipToken,
77
timeUntilStale,
88
} from './utils'
@@ -15,7 +15,6 @@ import type {
1515
CancelOptions,
1616
DefaultError,
1717
FetchStatus,
18-
InitialDataFunction,
1918
OmitKeyof,
2019
QueryFunction,
2120
QueryFunctionContext,
@@ -255,7 +254,8 @@ export class Query<
255254

256255
isActive(): boolean {
257256
return this.observers.some(
258-
(observer) => resolveEnabled(observer.options.enabled, this) !== false,
257+
(observer) =>
258+
resolveValueOrFunction(observer.options.enabled, this) !== false,
259259
)
260260
}
261261

@@ -659,17 +659,12 @@ function getDefaultState<
659659
>(
660660
options: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
661661
): QueryState<TData, TError> {
662-
const data =
663-
typeof options.initialData === 'function'
664-
? (options.initialData as InitialDataFunction<TData>)()
665-
: options.initialData
662+
const data = resolveValueOrFunction(options.initialData)
666663

667664
const hasData = data !== undefined
668665

669666
const initialDataUpdatedAt = hasData
670-
? typeof options.initialDataUpdatedAt === 'function'
671-
? (options.initialDataUpdatedAt as () => number | undefined)()
672-
: options.initialDataUpdatedAt
667+
? resolveValueOrFunction(options.initialDataUpdatedAt)
673668
: 0
674669

675670
return {

packages/query-core/src/queryClient.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
hashQueryKeyByOptions,
55
noop,
66
partialMatchKey,
7-
resolveStaleTime,
7+
resolveValueOrFunction,
88
skipToken,
99
} from './utils'
1010
import { QueryCache } from './queryCache'
@@ -156,7 +156,9 @@ export class QueryClient {
156156

157157
if (
158158
options.revalidateIfStale &&
159-
query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query))
159+
query.isStaleByTime(
160+
resolveValueOrFunction(defaultedOptions.staleTime, query),
161+
)
160162
) {
161163
void this.prefetchQuery(defaultedOptions)
162164
}
@@ -364,7 +366,7 @@ export class QueryClient {
364366
const query = this.#queryCache.build(this, defaultedOptions)
365367

366368
return query.isStaleByTime(
367-
resolveStaleTime(defaultedOptions.staleTime, query),
369+
resolveValueOrFunction(defaultedOptions.staleTime, query),
368370
)
369371
? query.fetch(defaultedOptions)
370372
: Promise.resolve(query.state.data as TData)

packages/query-core/src/queryObserver.ts

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import {
88
isValidTimeout,
99
noop,
1010
replaceData,
11-
resolveEnabled,
12-
resolveStaleTime,
11+
resolveValueOrFunction,
1312
shallowEqualObjects,
1413
timeUntilStale,
1514
} from './utils'
@@ -157,8 +156,10 @@ export class QueryObserver<
157156
this.options.enabled !== undefined &&
158157
typeof this.options.enabled !== 'boolean' &&
159158
typeof this.options.enabled !== 'function' &&
160-
typeof resolveEnabled(this.options.enabled, this.#currentQuery) !==
161-
'boolean'
159+
typeof resolveValueOrFunction(
160+
this.options.enabled,
161+
this.#currentQuery,
162+
) !== 'boolean'
162163
) {
163164
throw new Error(
164165
'Expected enabled to be a boolean or a callback that returns a boolean',
@@ -201,10 +202,10 @@ export class QueryObserver<
201202
if (
202203
mounted &&
203204
(this.#currentQuery !== prevQuery ||
204-
resolveEnabled(this.options.enabled, this.#currentQuery) !==
205-
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
206-
resolveStaleTime(this.options.staleTime, this.#currentQuery) !==
207-
resolveStaleTime(prevOptions.staleTime, this.#currentQuery))
205+
resolveValueOrFunction(this.options.enabled, this.#currentQuery) !==
206+
resolveValueOrFunction(prevOptions.enabled, this.#currentQuery) ||
207+
resolveValueOrFunction(this.options.staleTime, this.#currentQuery) !==
208+
resolveValueOrFunction(prevOptions.staleTime, this.#currentQuery))
208209
) {
209210
this.#updateStaleTimeout()
210211
}
@@ -215,8 +216,8 @@ export class QueryObserver<
215216
if (
216217
mounted &&
217218
(this.#currentQuery !== prevQuery ||
218-
resolveEnabled(this.options.enabled, this.#currentQuery) !==
219-
resolveEnabled(prevOptions.enabled, this.#currentQuery) ||
219+
resolveValueOrFunction(this.options.enabled, this.#currentQuery) !==
220+
resolveValueOrFunction(prevOptions.enabled, this.#currentQuery) ||
220221
nextRefetchInterval !== this.#currentRefetchInterval)
221222
) {
222223
this.#updateRefetchInterval(nextRefetchInterval)
@@ -344,7 +345,7 @@ export class QueryObserver<
344345

345346
#updateStaleTimeout(): void {
346347
this.#clearStaleTimeout()
347-
const staleTime = resolveStaleTime(
348+
const staleTime = resolveValueOrFunction(
348349
this.options.staleTime,
349350
this.#currentQuery,
350351
)
@@ -368,9 +369,10 @@ export class QueryObserver<
368369

369370
#computeRefetchInterval() {
370371
return (
371-
(typeof this.options.refetchInterval === 'function'
372-
? this.options.refetchInterval(this.#currentQuery)
373-
: this.options.refetchInterval) ?? false
372+
resolveValueOrFunction(
373+
this.options.refetchInterval,
374+
this.#currentQuery,
375+
) ?? false
374376
)
375377
}
376378

@@ -381,7 +383,8 @@ export class QueryObserver<
381383

382384
if (
383385
isServer ||
384-
resolveEnabled(this.options.enabled, this.#currentQuery) === false ||
386+
resolveValueOrFunction(this.options.enabled, this.#currentQuery) ===
387+
false ||
385388
!isValidTimeout(this.#currentRefetchInterval) ||
386389
this.#currentRefetchInterval === 0
387390
) {
@@ -489,15 +492,11 @@ export class QueryObserver<
489492
skipSelect = true
490493
} else {
491494
// compute placeholderData
492-
placeholderData =
493-
typeof options.placeholderData === 'function'
494-
? (
495-
options.placeholderData as unknown as PlaceholderDataFunction<TQueryData>
496-
)(
497-
this.#lastQueryWithDefinedData?.state.data,
498-
this.#lastQueryWithDefinedData as any,
499-
)
500-
: options.placeholderData
495+
placeholderData = resolveValueOrFunction(
496+
options.placeholderData,
497+
this.#lastQueryWithDefinedData?.state.data,
498+
this.#lastQueryWithDefinedData as any,
499+
)
501500
}
502501

503502
if (placeholderData !== undefined) {
@@ -660,9 +659,7 @@ export class QueryObserver<
660659

661660
const { notifyOnChangeProps } = this.options
662661
const notifyOnChangePropsValue =
663-
typeof notifyOnChangeProps === 'function'
664-
? notifyOnChangeProps()
665-
: notifyOnChangeProps
662+
resolveValueOrFunction(notifyOnChangeProps)
666663

667664
if (
668665
notifyOnChangePropsValue === 'all' ||
@@ -740,7 +737,7 @@ function shouldLoadOnMount(
740737
options: QueryObserverOptions<any, any, any, any>,
741738
): boolean {
742739
return (
743-
resolveEnabled(options.enabled, query) !== false &&
740+
resolveValueOrFunction(options.enabled, query) !== false &&
744741
query.state.data === undefined &&
745742
!(query.state.status === 'error' && options.retryOnMount === false)
746743
)
@@ -764,8 +761,8 @@ function shouldFetchOn(
764761
(typeof options)['refetchOnWindowFocus'] &
765762
(typeof options)['refetchOnReconnect'],
766763
) {
767-
if (resolveEnabled(options.enabled, query) !== false) {
768-
const value = typeof field === 'function' ? field(query) : field
764+
if (resolveValueOrFunction(options.enabled, query) !== false) {
765+
const value = resolveValueOrFunction(field, query)
769766

770767
return value === 'always' || (value !== false && isStale(query, options))
771768
}
@@ -780,7 +777,7 @@ function shouldFetchOptionally(
780777
): boolean {
781778
return (
782779
(query !== prevQuery ||
783-
resolveEnabled(prevOptions.enabled, query) === false) &&
780+
resolveValueOrFunction(prevOptions.enabled, query) === false) &&
784781
(!options.suspense || query.state.status !== 'error') &&
785782
isStale(query, options)
786783
)
@@ -791,8 +788,8 @@ function isStale(
791788
options: QueryObserverOptions<any, any, any, any, any>,
792789
): boolean {
793790
return (
794-
resolveEnabled(options.enabled, query) !== false &&
795-
query.isStaleByTime(resolveStaleTime(options.staleTime, query))
791+
resolveValueOrFunction(options.enabled, query) !== false &&
792+
query.isStaleByTime(resolveValueOrFunction(options.staleTime, query))
796793
)
797794
}
798795

packages/query-core/src/retryer.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { focusManager } from './focusManager'
22
import { onlineManager } from './onlineManager'
33
import { pendingThenable } from './thenable'
4-
import { isServer, sleep } from './utils'
4+
import { isServer, resolveValueOrFunction, sleep } from './utils'
55
import type { CancelOptions, DefaultError, NetworkMode } from './types'
66

77
// TYPES
@@ -166,10 +166,7 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
166166
// Do we need to retry the request?
167167
const retry = config.retry ?? (isServer ? 0 : 3)
168168
const retryDelay = config.retryDelay ?? defaultRetryDelay
169-
const delay =
170-
typeof retryDelay === 'function'
171-
? retryDelay(failureCount, error)
172-
: retryDelay
169+
const delay = resolveValueOrFunction(retryDelay, failureCount, error)
173170
const shouldRetry =
174171
retry === true ||
175172
(typeof retry === 'number' && failureCount < retry) ||

packages/query-core/src/utils.ts

Lines changed: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1+
import type { Mutation } from './mutation'
2+
import type { FetchOptions, Query } from './query'
13
import type {
24
DefaultError,
3-
Enabled,
45
FetchStatus,
56
MutationKey,
67
MutationStatus,
78
QueryFunction,
89
QueryKey,
910
QueryOptions,
10-
StaleTime,
1111
} from './types'
12-
import type { Mutation } from './mutation'
13-
import type { FetchOptions, Query } from './query'
1412

1513
// TYPES
1614

@@ -79,6 +77,94 @@ export function noop(): void
7977
export function noop(): undefined
8078
export function noop() {}
8179

80+
/**
81+
* Type guard that checks if a value is the function variant of a union type.
82+
*
83+
* This utility is designed for the common pattern in TanStack Query where options
84+
* can be either a direct value or a function that computes that value.
85+
*
86+
* @template T - The direct value type
87+
* @template TArgs - Array of argument types that the function variant accepts
88+
* @param value - The value to check, which can be either T or a function that returns something
89+
* @returns True if the value is a function, false otherwise. When true, TypeScript narrows the type to the function variant.
90+
*
91+
* @example
92+
* ```ts
93+
* // Basic usage with no arguments
94+
* const initialData: string | (() => string) = getValue()
95+
* if (isFunctionVariant(initialData)) {
96+
* // TypeScript knows initialData is () => string here
97+
* const result = initialData()
98+
* }
99+
* ```
100+
*
101+
* @example
102+
* ```ts
103+
* // Usage with function arguments
104+
* const staleTime: number | ((query: Query) => number) = getStaleTime()
105+
* if (isFunctionVariant<number, [Query]>(staleTime)) {
106+
* // TypeScript knows staleTime is (query: Query) => number here
107+
* const result = staleTime(query)
108+
* }
109+
* ```
110+
*/
111+
function isFunctionVariant<T, TArgs extends Array<any> = []>(
112+
value: T | ((...args: TArgs) => any),
113+
): value is (...args: TArgs) => any {
114+
return typeof value === 'function'
115+
}
116+
117+
/**
118+
* Resolves a value that can either be a direct value or a function that computes the value.
119+
*
120+
* This utility eliminates the need for repetitive `typeof value === 'function'` checks
121+
* throughout the codebase and provides a clean way to handle the common pattern where
122+
* options can be static values or dynamic functions.
123+
*
124+
* @template T - The type of the resolved value
125+
* @template TArgs - Array of argument types when resolving function variants
126+
* @param value - Either a direct value of type T or a function that returns T
127+
* @param args - Arguments to pass to the function if value is a function
128+
* @returns The resolved value of type T
129+
*
130+
* @example
131+
* ```ts
132+
* // Zero-argument function resolution (like initialData)
133+
* const initialData: string | (() => string) = 'hello'
134+
* const resolved = resolveValueOrFunction(initialData) // 'hello'
135+
*
136+
* const initialDataFn: string | (() => string) = () => 'world'
137+
* const resolved2 = resolveValueOrFunction(initialDataFn) // 'world'
138+
* ```
139+
*
140+
* @example
141+
* ```ts
142+
* // Function with arguments (like staleTime, retryDelay)
143+
* const staleTime: number | ((query: Query) => number) = (query) => query.state.dataUpdatedAt + 5000
144+
* const resolved = resolveValueOrFunction(staleTime, query) // number
145+
*
146+
* const retryDelay: number | ((failureCount: number, error: Error) => number) = 1000
147+
* const resolved2 = resolveValueOrFunction(retryDelay, 3, new Error()) // 1000
148+
* ```
149+
*
150+
* @example
151+
* ```ts
152+
* // Replaces verbose patterns like:
153+
* // const delay = typeof retryDelay === 'function'
154+
* // ? retryDelay(failureCount, error)
155+
* // : retryDelay
156+
*
157+
* // With:
158+
* const delay = resolveValueOrFunction(retryDelay, failureCount, error)
159+
* ```
160+
*/
161+
export function resolveValueOrFunction<T, TArgs extends Array<any>>(
162+
value: T | ((...args: TArgs) => T),
163+
...args: TArgs
164+
): T {
165+
return isFunctionVariant(value) ? value(...args) : value
166+
}
167+
82168
export function functionalUpdate<TInput, TOutput>(
83169
updater: Updater<TInput, TOutput>,
84170
input: TInput,
@@ -96,30 +182,6 @@ export function timeUntilStale(updatedAt: number, staleTime?: number): number {
96182
return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0)
97183
}
98184

99-
export function resolveStaleTime<
100-
TQueryFnData = unknown,
101-
TError = DefaultError,
102-
TData = TQueryFnData,
103-
TQueryKey extends QueryKey = QueryKey,
104-
>(
105-
staleTime: undefined | StaleTime<TQueryFnData, TError, TData, TQueryKey>,
106-
query: Query<TQueryFnData, TError, TData, TQueryKey>,
107-
): number | undefined {
108-
return typeof staleTime === 'function' ? staleTime(query) : staleTime
109-
}
110-
111-
export function resolveEnabled<
112-
TQueryFnData = unknown,
113-
TError = DefaultError,
114-
TData = TQueryFnData,
115-
TQueryKey extends QueryKey = QueryKey,
116-
>(
117-
enabled: undefined | Enabled<TQueryFnData, TError, TData, TQueryKey>,
118-
query: Query<TQueryFnData, TError, TData, TQueryKey>,
119-
): boolean | undefined {
120-
return typeof enabled === 'function' ? enabled(query) : enabled
121-
}
122-
123185
export function matchQuery(
124186
filters: QueryFilters,
125187
query: Query<any, any, any, any>,

0 commit comments

Comments
 (0)