diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index 6005bda0d3..4e31e029f3 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -2,8 +2,7 @@ import { ensureQueryFn, noop, replaceData, - resolveEnabled, - resolveStaleTime, + resolveOption, skipToken, timeUntilStale, } from './utils' @@ -16,7 +15,6 @@ import type { CancelOptions, DefaultError, FetchStatus, - InitialDataFunction, OmitKeyof, QueryFunctionContext, QueryKey, @@ -267,7 +265,7 @@ export class Query< isActive(): boolean { return this.observers.some( - (observer) => resolveEnabled(observer.options.enabled, this) !== false, + (observer) => resolveOption(observer.options.enabled, this) !== false, ) } @@ -286,7 +284,7 @@ export class Query< if (this.getObserversCount() > 0) { return this.observers.some( (observer) => - resolveStaleTime(observer.options.staleTime, this) === 'static', + resolveOption(observer.options.staleTime, this) === 'static', ) } @@ -389,7 +387,7 @@ export class Query< ): Promise { if ( this.state.fetchStatus !== 'idle' && - // If the promise in the retyer is already rejected, we have to definitely + // If the promise in the retryer is already rejected, we have to definitely // re-start the fetch; there is a chance that the query is still in a // pending state when that happens this.#retryer?.status() !== 'rejected' @@ -726,17 +724,12 @@ function getDefaultState< >( options: QueryOptions, ): QueryState { - const data = - typeof options.initialData === 'function' - ? (options.initialData as InitialDataFunction)() - : options.initialData + const data = resolveOption(options.initialData) as TData | undefined const hasData = data !== undefined const initialDataUpdatedAt = hasData - ? typeof options.initialDataUpdatedAt === 'function' - ? (options.initialDataUpdatedAt as () => number | undefined)() - : options.initialDataUpdatedAt + ? resolveOption(options.initialDataUpdatedAt) : 0 return { diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 80cc36668a..ac41c9ceca 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -1,10 +1,9 @@ import { - functionalUpdate, hashKey, hashQueryKeyByOptions, noop, partialMatchKey, - resolveStaleTime, + resolveOption, skipToken, } from './utils' import { QueryCache } from './queryCache' @@ -155,7 +154,7 @@ export class QueryClient { if ( options.revalidateIfStale && - query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query)) + query.isStaleByTime(resolveOption(defaultedOptions.staleTime, query)) ) { void this.prefetchQuery(defaultedOptions) } @@ -197,7 +196,7 @@ export class QueryClient { defaultedOptions.queryHash, ) const prevData = query?.state.data - const data = functionalUpdate(updater, prevData) + const data = resolveOption(updater, prevData) if (data === undefined) { return undefined @@ -362,9 +361,7 @@ export class QueryClient { const query = this.#queryCache.build(this, defaultedOptions) - return query.isStaleByTime( - resolveStaleTime(defaultedOptions.staleTime, query), - ) + return query.isStaleByTime(resolveOption(defaultedOptions.staleTime, query)) ? query.fetch(defaultedOptions) : Promise.resolve(query.state.data as TData) } diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 92978673f6..6cd8584887 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -8,8 +8,7 @@ import { isValidTimeout, noop, replaceData, - resolveEnabled, - resolveStaleTime, + resolveOption, shallowEqualObjects, timeUntilStale, } from './utils' @@ -21,7 +20,6 @@ import type { PendingThenable, Thenable } from './thenable' import type { DefaultError, DefaultedQueryObserverOptions, - PlaceholderDataFunction, QueryKey, QueryObserverBaseResult, QueryObserverOptions, @@ -154,7 +152,7 @@ export class QueryObserver< this.options.enabled !== undefined && typeof this.options.enabled !== 'boolean' && typeof this.options.enabled !== 'function' && - typeof resolveEnabled(this.options.enabled, this.#currentQuery) !== + typeof resolveOption(this.options.enabled, this.#currentQuery) !== 'boolean' ) { throw new Error( @@ -198,10 +196,10 @@ export class QueryObserver< if ( mounted && (this.#currentQuery !== prevQuery || - resolveEnabled(this.options.enabled, this.#currentQuery) !== - resolveEnabled(prevOptions.enabled, this.#currentQuery) || - resolveStaleTime(this.options.staleTime, this.#currentQuery) !== - resolveStaleTime(prevOptions.staleTime, this.#currentQuery)) + resolveOption(this.options.enabled, this.#currentQuery) !== + resolveOption(prevOptions.enabled, this.#currentQuery) || + resolveOption(this.options.staleTime, this.#currentQuery) !== + resolveOption(prevOptions.staleTime, this.#currentQuery)) ) { this.#updateStaleTimeout() } @@ -212,8 +210,8 @@ export class QueryObserver< if ( mounted && (this.#currentQuery !== prevQuery || - resolveEnabled(this.options.enabled, this.#currentQuery) !== - resolveEnabled(prevOptions.enabled, this.#currentQuery) || + resolveOption(this.options.enabled, this.#currentQuery) !== + resolveOption(prevOptions.enabled, this.#currentQuery) || nextRefetchInterval !== this.#currentRefetchInterval) ) { this.#updateRefetchInterval(nextRefetchInterval) @@ -354,10 +352,7 @@ export class QueryObserver< #updateStaleTimeout(): void { this.#clearStaleTimeout() - const staleTime = resolveStaleTime( - this.options.staleTime, - this.#currentQuery, - ) + const staleTime = resolveOption(this.options.staleTime, this.#currentQuery) if (isServer || this.#currentResult.isStale || !isValidTimeout(staleTime)) { return @@ -378,9 +373,7 @@ export class QueryObserver< #computeRefetchInterval() { return ( - (typeof this.options.refetchInterval === 'function' - ? this.options.refetchInterval(this.#currentQuery) - : this.options.refetchInterval) ?? false + resolveOption(this.options.refetchInterval, this.#currentQuery) ?? false ) } @@ -391,7 +384,7 @@ export class QueryObserver< if ( isServer || - resolveEnabled(this.options.enabled, this.#currentQuery) === false || + resolveOption(this.options.enabled, this.#currentQuery) === false || !isValidTimeout(this.#currentRefetchInterval) || this.#currentRefetchInterval === 0 ) { @@ -499,15 +492,11 @@ export class QueryObserver< skipSelect = true } else { // compute placeholderData - placeholderData = - typeof options.placeholderData === 'function' - ? ( - options.placeholderData as unknown as PlaceholderDataFunction - )( - this.#lastQueryWithDefinedData?.state.data, - this.#lastQueryWithDefinedData as any, - ) - : options.placeholderData + placeholderData = resolveOption( + options.placeholderData, + this.#lastQueryWithDefinedData?.state.data, + this.#lastQueryWithDefinedData as any, + ) } if (placeholderData !== undefined) { @@ -586,7 +575,7 @@ export class QueryObserver< isStale: isStale(query, options), refetch: this.refetch, promise: this.#currentThenable, - isEnabled: resolveEnabled(options.enabled, query) !== false, + isEnabled: resolveOption(options.enabled, query) !== false, } const nextResult = result as QueryObserverResult @@ -670,10 +659,7 @@ export class QueryObserver< } const { notifyOnChangeProps } = this.options - const notifyOnChangePropsValue = - typeof notifyOnChangeProps === 'function' - ? notifyOnChangeProps() - : notifyOnChangeProps + const notifyOnChangePropsValue = resolveOption(notifyOnChangeProps) if ( notifyOnChangePropsValue === 'all' || @@ -751,7 +737,7 @@ function shouldLoadOnMount( options: QueryObserverOptions, ): boolean { return ( - resolveEnabled(options.enabled, query) !== false && + resolveOption(options.enabled, query) !== false && query.state.data === undefined && !(query.state.status === 'error' && options.retryOnMount === false) ) @@ -776,10 +762,10 @@ function shouldFetchOn( (typeof options)['refetchOnReconnect'], ) { if ( - resolveEnabled(options.enabled, query) !== false && - resolveStaleTime(options.staleTime, query) !== 'static' + resolveOption(options.enabled, query) !== false && + resolveOption(options.staleTime, query) !== 'static' ) { - const value = typeof field === 'function' ? field(query) : field + const value = resolveOption(field, query) return value === 'always' || (value !== false && isStale(query, options)) } @@ -794,7 +780,7 @@ function shouldFetchOptionally( ): boolean { return ( (query !== prevQuery || - resolveEnabled(prevOptions.enabled, query) === false) && + resolveOption(prevOptions.enabled, query) === false) && (!options.suspense || query.state.status !== 'error') && isStale(query, options) ) @@ -805,8 +791,8 @@ function isStale( options: QueryObserverOptions, ): boolean { return ( - resolveEnabled(options.enabled, query) !== false && - query.isStaleByTime(resolveStaleTime(options.staleTime, query)) + resolveOption(options.enabled, query) !== false && + query.isStaleByTime(resolveOption(options.staleTime, query)) ) } diff --git a/packages/query-core/src/retryer.ts b/packages/query-core/src/retryer.ts index f4ada851c9..0eb4164a76 100644 --- a/packages/query-core/src/retryer.ts +++ b/packages/query-core/src/retryer.ts @@ -1,7 +1,7 @@ import { focusManager } from './focusManager' import { onlineManager } from './onlineManager' import { pendingThenable } from './thenable' -import { isServer, sleep } from './utils' +import { isServer, resolveOption, sleep } from './utils' import type { Thenable } from './thenable' import type { CancelOptions, DefaultError, NetworkMode } from './types' @@ -168,10 +168,7 @@ export function createRetryer( // Do we need to retry the request? const retry = config.retry ?? (isServer ? 0 : 3) const retryDelay = config.retryDelay ?? defaultRetryDelay - const delay = - typeof retryDelay === 'function' - ? retryDelay(failureCount, error) - : retryDelay + const delay = resolveOption(retryDelay, failureCount, error) const shouldRetry = retry === true || (typeof retry === 'number' && failureCount < retry) || diff --git a/packages/query-core/src/streamedQuery.ts b/packages/query-core/src/streamedQuery.ts index 8fa57c5ab8..cb33cfcef7 100644 --- a/packages/query-core/src/streamedQuery.ts +++ b/packages/query-core/src/streamedQuery.ts @@ -114,7 +114,7 @@ export function streamedQuery< // finalize result: replace-refetching needs to write to the cache if (isReplaceRefetch && !cancelled) { - context.client.setQueryData(context.queryKey, result) + context.client.setQueryData(context.queryKey, () => result) } return context.client.getQueryData(context.queryKey) ?? initialValue diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 4f3f4caed2..0b90817a64 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -166,7 +166,13 @@ export type QueryFunctionContext< export type InitialDataFunction = () => T | undefined -type NonFunctionGuard = T extends Function ? never : T +/** + * `NonFunctionGuard` ensures T is not a function type. + * + * If T is a function, it resolves to `never`, effectively removing T + * from unions and preventing ambiguity in value-or-function patterns. + */ +export type NonFunctionGuard = T extends Function ? never : T export type PlaceholderDataFunction< TQueryFnData = unknown, diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index feee9c36c6..bee2a61626 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -1,18 +1,16 @@ import { timeoutManager } from './timeoutManager' +import type { Mutation } from './mutation' +import type { FetchOptions, Query } from './query' import type { DefaultError, - Enabled, FetchStatus, MutationKey, MutationStatus, + NonFunctionGuard, QueryFunction, QueryKey, QueryOptions, - StaleTime, - StaleTimeFunction, } from './types' -import type { Mutation } from './mutation' -import type { FetchOptions, Query } from './query' // TYPES @@ -80,7 +78,9 @@ export interface MutationFilters< status?: MutationStatus } -export type Updater = TOutput | ((input: TInput) => TOutput) +export type Updater = + | NonFunctionGuard + | ((input: TInput) => TOutput) export type QueryTypeFilter = 'all' | 'active' | 'inactive' @@ -92,13 +92,51 @@ export function noop(): void export function noop(): undefined export function noop() {} -export function functionalUpdate( - updater: Updater, - input: TInput, -): TOutput { - return typeof updater === 'function' - ? (updater as (_: TInput) => TOutput)(input) - : updater +type ResolvedValue = TValueOrFn extends ( + ...args: Array +) => infer R + ? R + : TValueOrFn + +/** + * Resolves a value that can either be a direct value or a function that computes the value. + * + * This utility eliminates the need for repetitive `typeof value === 'function'` checks + * throughout the codebase. It uses input-driven type inference, meaning it infers the + * return type from the actual input type rather than requiring explicit generic constraints. + * + * @example + * ```ts + * // Zero-argument function resolution (like initialData) + * const initialData: string | (() => string) = 'hello' + * const resolved = resolveOption(initialData) // string + * + * // Function with arguments (like staleTime, retryDelay) + * const staleTime: number | ((query: Query) => number) = 1000 + * const resolved = resolveOption(staleTime, query) // number + * + * // Works with generics (TData, TQueryData, etc.) + * const placeholderData: TData | ((prev: TData) => TData) = ... + * const resolved = resolveOption(placeholderData, prevData) // TData + * ``` + * + * @remarks + * If the resolved value itself needs to be a function, wrap it: + * `resolveOption(() => myFunction)` or `setQueryData(key, () => myFunction)` + */ +export function resolveOption( + valueOrFn: TValueOrFn, + ...args: Array +): ResolvedValue +export function resolveOption( + valueOrFn: TValueOrFn | undefined, + ...args: Array +): ResolvedValue | undefined +export function resolveOption( + valueOrFn: unknown, + ...args: Array +): unknown { + return typeof valueOrFn === 'function' ? valueOrFn(...args) : valueOrFn } export function isValidTimeout(value: unknown): value is number { @@ -109,32 +147,6 @@ export function timeUntilStale(updatedAt: number, staleTime?: number): number { return Math.max(updatedAt + (staleTime || 0) - Date.now(), 0) } -export function resolveStaleTime< - TQueryFnData = unknown, - TError = DefaultError, - TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey, ->( - staleTime: - | undefined - | StaleTimeFunction, - query: Query, -): StaleTime | undefined { - return typeof staleTime === 'function' ? staleTime(query) : staleTime -} - -export function resolveEnabled< - TQueryFnData = unknown, - TError = DefaultError, - TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey, ->( - enabled: undefined | Enabled, - query: Query, -): boolean | undefined { - return typeof enabled === 'function' ? enabled(query) : enabled -} - export function matchQuery( filters: QueryFilters, query: Query,