Skip to content

Commit d70abce

Browse files
committed
fix(useQuery): make sure queryKeys are always an array at runtime when passed to the queryFn
this is a regression from #2074 the real culprit we tried to fix was typings being still just TQueryKey in the FetchContext, which is not what it is at runtime, because if your QueryKey is a string, it will be [string] there. See: https://github.com/tannerlinsley/react-query/pull/2074/files#r606362656 this fix re-adds the `ensureArray` call, but makes it narrow the type (compromised type assertion) so that the FetchContext will receive an `EnsuredQueryKey`, which is always an Array
1 parent 37a3118 commit d70abce

File tree

4 files changed

+44
-16
lines changed

4 files changed

+44
-16
lines changed

src/core/query.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {
55
noop,
66
replaceEqualDeep,
77
timeUntilStale,
8+
ensureQueryKeyArray,
89
} from './utils'
910
import type {
1011
InitialDataFunction,
1112
QueryKey,
1213
QueryOptions,
1314
QueryStatus,
1415
QueryFunctionContext,
16+
EnsuredQueryKey,
1517
} from './types'
1618
import type { QueryCache } from './queryCache'
1719
import type { QueryObserver } from './queryObserver'
@@ -58,8 +60,8 @@ export interface FetchContext<
5860
> {
5961
fetchFn: () => unknown | Promise<unknown>
6062
fetchOptions?: FetchOptions
61-
options: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
62-
queryKey: TQueryKey
63+
options: QueryOptions<TQueryFnData, TError, TData, any>
64+
queryKey: EnsuredQueryKey<TQueryKey>
6365
state: QueryState<TData, TError>
6466
}
6567

@@ -376,9 +378,11 @@ export class Query<
376378
}
377379
}
378380

381+
const queryKey = ensureQueryKeyArray(this.queryKey)
382+
379383
// Create query function context
380384
const queryFnContext: QueryFunctionContext<TQueryKey> = {
381-
queryKey: this.queryKey,
385+
queryKey,
382386
pageParam: undefined,
383387
}
384388

@@ -389,10 +393,10 @@ export class Query<
389393
: Promise.reject('Missing queryFn')
390394

391395
// Trigger behavior hook
392-
const context: FetchContext<TQueryFnData, TError, TData, any> = {
396+
const context: FetchContext<TQueryFnData, TError, TData, TQueryKey> = {
393397
fetchOptions,
394398
options: this.options,
395-
queryKey: this.queryKey,
399+
queryKey: queryKey,
396400
state: this.state,
397401
fetchFn,
398402
}

src/core/types.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import type { RetryValue, RetryDelayValue } from './retryer'
44
import type { QueryFilters } from './utils'
55

66
export type QueryKey = string | readonly unknown[]
7+
export type EnsuredQueryKey<T extends QueryKey> = T extends string
8+
? [T]
9+
: Exclude<T, string>
710

811
export type QueryFunction<
912
T = unknown,
@@ -14,14 +17,12 @@ export interface QueryFunctionContext<
1417
TQueryKey extends QueryKey = QueryKey,
1518
TPageParam = any
1619
> {
17-
queryKey: TQueryKey
20+
queryKey: EnsuredQueryKey<TQueryKey>
1821
pageParam?: TPageParam
1922
}
2023

2124
export type InitialDataFunction<T> = () => T | undefined
2225

23-
export type InitialStaleFunction = () => boolean
24-
2526
export type PlaceholderDataFunction<TResult> = () => TResult | undefined
2627

2728
export type QueryKeyHashFunction<TQueryKey extends QueryKey> = (
@@ -225,7 +226,13 @@ export interface FetchInfiniteQueryOptions<
225226
TError = unknown,
226227
TData = TQueryFnData,
227228
TQueryKey extends QueryKey = QueryKey
228-
> extends FetchQueryOptions<TQueryFnData, TError, InfiniteData<TData>, TQueryKey> {}
229+
>
230+
extends FetchQueryOptions<
231+
TQueryFnData,
232+
TError,
233+
InfiniteData<TData>,
234+
TQueryKey
235+
> {}
229236

230237
export interface ResultOptions {
231238
throwOnError?: boolean
@@ -474,7 +481,9 @@ export interface MutationOptions<
474481
mutationFn?: MutationFunction<TData, TVariables>
475482
mutationKey?: MutationKey
476483
variables?: TVariables
477-
onMutate?: (variables: TVariables) => Promise<TContext> | Promise<undefined> | TContext | undefined
484+
onMutate?: (
485+
variables: TVariables
486+
) => Promise<TContext> | Promise<undefined> | TContext | undefined
478487
onSuccess?: (
479488
data: TData,
480489
variables: TVariables,

src/core/utils.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Mutation } from './mutation'
22
import type { Query } from './query'
3+
import { EnsuredQueryKey } from './types'
34
import type {
45
MutationFunction,
56
MutationKey,
@@ -88,8 +89,12 @@ export function isValidTimeout(value: any): value is number {
8889
return typeof value === 'number' && value >= 0 && value !== Infinity
8990
}
9091

91-
export function ensureArray<T>(value: T | T[]): T[] {
92-
return Array.isArray(value) ? value : [value]
92+
export function ensureQueryKeyArray<T extends QueryKey>(
93+
value: T
94+
): EnsuredQueryKey<T> {
95+
return (Array.isArray(value)
96+
? value
97+
: ([value] as unknown)) as EnsuredQueryKey<T>
9398
}
9499

95100
export function difference<T>(array1: T[], array2: T[]): T[] {
@@ -256,7 +261,7 @@ export function hashQueryKeyByOptions<TQueryKey extends QueryKey = QueryKey>(
256261
* Default query keys hash function.
257262
*/
258263
export function hashQueryKey(queryKey: QueryKey): string {
259-
const asArray = Array.isArray(queryKey) ? queryKey : [queryKey]
264+
const asArray = ensureQueryKeyArray(queryKey)
260265
return stableValueHash(asArray)
261266
}
262267

@@ -280,7 +285,7 @@ export function stableValueHash(value: any): string {
280285
* Checks if key `b` partially matches with key `a`.
281286
*/
282287
export function partialMatchKey(a: QueryKey, b: QueryKey): boolean {
283-
return partialDeepEqual(ensureArray(a), ensureArray(b))
288+
return partialDeepEqual(ensureQueryKeyArray(a), ensureQueryKeyArray(b))
284289
}
285290

286291
/**

src/react/tests/useQuery.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,25 @@ describe('useQuery', () => {
7979
type MyData = number
8080
type MyQueryKey = readonly ['my-data', number]
8181

82-
const getMyData: QueryFunction<MyData, MyQueryKey> = async ({
82+
const getMyDataArrayKey: QueryFunction<MyData, MyQueryKey> = async ({
8383
queryKey: [, n],
8484
}) => {
8585
return n + 42
8686
}
8787

8888
useQuery({
8989
queryKey: ['my-data', 100],
90-
queryFn: getMyData,
90+
queryFn: getMyDataArrayKey,
91+
})
92+
93+
const getMyDataStringKey: QueryFunction<MyData, '1'> = async context => {
94+
expectType<['1']>(context.queryKey)
95+
return Number(context.queryKey[0]) + 42
96+
}
97+
98+
useQuery({
99+
queryKey: '1',
100+
queryFn: getMyDataStringKey,
91101
})
92102
}
93103
})

0 commit comments

Comments
 (0)