Skip to content

feat(query-core, react-query, vue-query): backport v5 some apis to v4 #9140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 26, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v16.19.0
22.12.0
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -75,6 +75,7 @@
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"expect-type": "^1.2.1",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using expect-type(used by vitest internally) here to ensure type safety in our type tests for useSuspenseQuery, useSuspenseQueries, queryOptions.

image

"git-log-parser": "^1.2.0",
"jest": "^27.5.1",
"jsonfile": "^6.1.0",
4 changes: 3 additions & 1 deletion packages/query-core/src/mutationObserver.ts
Original file line number Diff line number Diff line change
@@ -141,14 +141,16 @@ export class MutationObserver<
? this.currentMutation.state
: getDefaultState<TData, TError, TVariables, TContext>()

const isLoading = state.status === 'loading'
const result: MutationObserverBaseResult<
TData,
TError,
TVariables,
TContext
> = {
...state,
isLoading: state.status === 'loading',
isLoading,
isPending: isLoading,
Comment on lines +152 to +153
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use isPending like v5

isSuccess: state.status === 'success',
isError: state.status === 'error',
isIdle: state.status === 'idle',
43 changes: 37 additions & 6 deletions packages/query-core/src/queryCache.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { notifyManager } from './notifyManager'
import { Subscribable } from './subscribable'
import type { QueryFilters } from './utils'
import type { Action, QueryState } from './query'
import type { NotifyEvent, QueryKey, QueryOptions } from './types'
import type { NotifyEvent, OmitKeyof, QueryKey, QueryOptions } from './types'
import type { QueryClient } from './queryClient'
import type { QueryObserver } from './queryObserver'

@@ -166,8 +166,21 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
}

find<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
arg1: QueryKey,
arg2?: QueryFilters,
filters: QueryFilters,
): Query<TQueryFnData, TError, TData> | undefined
Comment on lines 168 to +170
Copy link
Collaborator Author

@manudeli manudeli May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a function overload accepting only a filters object. This could make migration to v5 easier. In v5, QueryCache.find only accepts a filters object

/**
* @deprecated This method should be used with only one object argument.
*/
find<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
queryKey: QueryKey,
filters?: OmitKeyof<QueryFilters, 'queryKey'>,
): Query<TQueryFnData, TError, TData> | undefined
/**
* @deprecated This method should be used with only one object argument.
*/
find<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
arg1: QueryKey | QueryFilters,
arg2?: OmitKeyof<QueryFilters, 'queryKey'>,
Comment on lines +171 to +183
Copy link
Collaborator Author

@manudeli manudeli May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tagged jsdoc @deprecated on unnecessary type overriding of QueryCache's methods

): Query<TQueryFnData, TError, TData> | undefined {
const [filters] = parseFilterArgs(arg1, arg2)

@@ -178,10 +191,28 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
return this.queries.find((query) => matchQuery(filters, query))
}

findAll(queryKey?: QueryKey, filters?: QueryFilters): Query[]
findAll(filters?: QueryFilters): Query[]
findAll(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters): Query[]
findAll(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters): Query[] {
/**
* @deprecated This method should be used with only one object argument.
*/
findAll(
queryKey?: QueryKey,
filters?: OmitKeyof<QueryFilters, 'queryKey'>,
): Query[]
/**
* @deprecated This method should be used with only one object argument.
*/
findAll(
arg1?: QueryKey | QueryFilters,
arg2?: OmitKeyof<QueryFilters, 'queryKey'>,
): Query[]
/**
* @deprecated This method should be used with only one object argument.
*/
findAll(
arg1?: QueryKey | QueryFilters,
arg2?: OmitKeyof<QueryFilters, 'queryKey'>,
): Query[] {
Comment on lines +195 to +215
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tagged jsdoc @deprecated on unnecessary type overriding of QueryCache's methods

const [filters] = parseFilterArgs(arg1, arg2)
return Object.keys(filters).length > 0
? this.queries.filter((query) => matchQuery(filters, query))
239 changes: 202 additions & 37 deletions packages/query-core/src/queryClient.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/query-core/src/tests/mutations.test.tsx
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ describe('mutations', () => {
isError: false,
isIdle: true,
isLoading: false,
isPending: false,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
@@ -103,6 +104,7 @@ describe('mutations', () => {
isError: false,
isIdle: false,
isLoading: true,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
@@ -122,6 +124,7 @@ describe('mutations', () => {
isError: false,
isIdle: false,
isLoading: true,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
@@ -141,6 +144,7 @@ describe('mutations', () => {
isError: false,
isIdle: false,
isLoading: false,
isPending: false,
isPaused: false,
isSuccess: true,
mutate: expect.any(Function),
@@ -181,6 +185,7 @@ describe('mutations', () => {
isError: false,
isIdle: false,
isLoading: true,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
@@ -200,6 +205,7 @@ describe('mutations', () => {
isError: false,
isIdle: false,
isLoading: true,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
@@ -219,6 +225,7 @@ describe('mutations', () => {
isError: false,
isIdle: false,
isLoading: true,
isPending: true,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
@@ -238,6 +245,7 @@ describe('mutations', () => {
isError: true,
isIdle: false,
isLoading: false,
isPending: false,
isPaused: false,
isSuccess: false,
mutate: expect.any(Function),
13 changes: 13 additions & 0 deletions packages/query-core/src/tests/queryClient.types.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { doNotExecute, queryKey } from './utils'
import type { QueryClient } from '..'

describe('queryClient', () => {
let queryClient: QueryClient

it('should be used with queryCache', () => {
doNotExecute(() => {
queryClient.getQueryData(queryKey())
queryClient.getQueryData(queryKey(), {})
})
})
})
2 changes: 2 additions & 0 deletions packages/query-core/src/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ import { QueryClient } from '@tanstack/query-core'
import * as utils from '../utils'
import type { MutationOptions, QueryClientConfig } from '@tanstack/query-core'

export const doNotExecute = (_func: () => void) => true

export function createQueryClient(config?: QueryClientConfig): QueryClient {
jest.spyOn(console, 'error').mockImplementation(() => undefined)
return new QueryClient({ logger: mockLogger, ...config })
20 changes: 20 additions & 0 deletions packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,20 @@ import type { QueryCache } from './queryCache'
import type { MutationCache } from './mutationCache'
import type { Logger } from './logger'

export type NonUndefinedGuard<T> = T extends undefined ? never : T

export type OmitKeyof<
TObject,
TKey extends TStrictly extends 'safely'
?
| keyof TObject
| (string & Record<never, never>)
| (number & Record<never, never>)
| (symbol & Record<never, never>)
: keyof TObject,
TStrictly extends 'strictly' | 'safely' = 'strictly',
> = Omit<TObject, TKey>

export type QueryKey = readonly unknown[]

export type QueryFunction<
@@ -256,6 +270,8 @@ export interface QueryObserverOptions<
/**
* Set this to `true` to keep the previous `data` when fetching based on a new query key.
* Defaults to `false`.
*
* @deprecated keepPreviousData will be removed in the next major version.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tagged @deprecated

*/
keepPreviousData?: boolean
/**
@@ -406,6 +422,9 @@ export interface QueryObserverBaseResult<TData = unknown, TError = unknown> {
refetch: <TPageData>(
options?: RefetchOptions & RefetchQueryFilters<TPageData>,
) => Promise<QueryObserverResult<TData, TError>>
/**
* @deprecated This method will be removed in the next major version. Use `QueryClient.removeQueries` instead.
*/
remove: () => void
status: QueryStatus
fetchStatus: FetchStatus
@@ -646,6 +665,7 @@ export interface MutationObserverBaseResult<
isError: boolean
isIdle: boolean
isLoading: boolean
isPending: boolean
isSuccess: boolean
mutate: MutateFunction<TData, TError, TVariables, TContext>
reset: () => void
270 changes: 270 additions & 0 deletions packages/react-query/src/__tests__/queryOptions.types.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { expectTypeOf } from 'expect-type'
import {
QueryCache,
type UseQueryResult,
useQueries,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { queryOptions } from '..'
import { useSuspenseQueries, useSuspenseQuery } from '..'
import { type UseSuspenseQueryResult } from '../useSuspenseQuery'
import { doNotExecute } from './utils'
import type { DefinedUseQueryResult } from '@tanstack/react-query'

const queryKey = ['key'] as const
const queryFn = () => Promise.resolve({ field: 'success' })

describe('queryOptions', () => {
it('should be used with useQuery', () => {
doNotExecute(() => {
const dd = useQuery(
queryOptions({
queryKey,
queryFn,
}),
)
expectTypeOf(dd).toEqualTypeOf<UseQueryResult<{ field: string }>>()
expectTypeOf(
useQuery({
...queryOptions({
queryKey,
queryFn,
}),
select: (data) => data.field,
}),
).toEqualTypeOf<UseQueryResult<string>>()
expectTypeOf(
useQuery({
...queryOptions({
queryKey,
queryFn,
select: (data) => data.field,
}),
}),
).toEqualTypeOf<UseQueryResult<string>>()
expectTypeOf(
useQuery({
...queryOptions({
queryKey,
queryFn,
}),
initialData: { field: 'success' },
}),
).toEqualTypeOf<DefinedUseQueryResult<{ field: string }>>()
expectTypeOf(
useQuery({
...queryOptions({
queryKey,
queryFn,
}),
initialData: { field: 'success' },
select: (data) => data.field,
}),
).toEqualTypeOf<DefinedUseQueryResult<string>>()
expectTypeOf(
useQuery({
...queryOptions({
queryKey,
queryFn,
}),
initialData: undefined,
select: (data) => data.field,
}),
).toEqualTypeOf<UseQueryResult<string>>()
expectTypeOf(
useQuery({
...queryOptions({
queryKey,
queryFn,
}),
initialData: () => undefined,
select: (data) => data.field,
}),
).toEqualTypeOf<UseQueryResult<string>>()
expectTypeOf(
useQuery({
...queryOptions({
queryKey,
queryFn,
select: (data) => data.field,
}),
refetchInterval: 1000,
}),
).toEqualTypeOf<UseQueryResult<string>>()
})
})
it('should be used with useSuspenseQuery', () => {
doNotExecute(() => {
expectTypeOf(
useSuspenseQuery(
queryOptions({
queryKey,
queryFn,
}),
),
).toEqualTypeOf<UseSuspenseQueryResult<{ field: string }>>()

expectTypeOf(
useSuspenseQuery({
...queryOptions({
queryKey,
queryFn,
}),
select: (data) => data.field,
}),
).toEqualTypeOf<UseSuspenseQueryResult<string>>()
expectTypeOf(
useSuspenseQuery({
...queryOptions({
queryKey,
queryFn,
}),
initialData: { field: 'success' },
}),
).toEqualTypeOf<UseSuspenseQueryResult<{ field: string }>>()
expectTypeOf(
useSuspenseQuery({
...queryOptions({
queryKey,
queryFn,
}),
initialData: undefined,
select: (data) => data.field,
}),
).toEqualTypeOf<UseSuspenseQueryResult<string>>()
expectTypeOf(
useSuspenseQuery({
...queryOptions({
queryKey,
queryFn,
}),
initialData: { field: 'success' },
select: (data) => data.field,
}),
).toEqualTypeOf<UseSuspenseQueryResult<string>>()
expectTypeOf(
useSuspenseQuery({
...queryOptions({
queryKey,
queryFn,
select: (data) => data.field,
}),
refetchInterval: 1000,
}),
).toEqualTypeOf<UseSuspenseQueryResult<string>>()
})
})
it('should be used with useQueries', () => {
doNotExecute(() => {
const [query1, query2, query3, query4] = useQueries({
queries: [
queryOptions({
queryKey,
queryFn,
}),
queryOptions({
queryKey,
queryFn,
initialData: { field: 'success' },
}),
{
...queryOptions({
queryKey,
queryFn,
}),
initialData: { field: 'success' },
},
{
...queryOptions({
queryKey,
queryFn,
}),
initialData: undefined,
},
],
})
expectTypeOf(query1).toEqualTypeOf<UseQueryResult<{ field: string }>>()
expectTypeOf(query2).toEqualTypeOf<
DefinedUseQueryResult<{ field: string }>
>()
expectTypeOf(query3).toEqualTypeOf<
DefinedUseQueryResult<{ field: string }>
>()
Comment on lines +188 to +193
Copy link
Collaborator Author

@manudeli manudeli May 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the issue where useQueries had to return DefinedUseQueryResult.
This was previously returning UseQueryResult instead of DefinedUseQueryResult.

expectTypeOf(query4).toEqualTypeOf<UseQueryResult<{ field: string }>>()
})
})
it('should be used with useSuspenseQueries', () => {
doNotExecute(() => {
const [query1, query2, query3, query4, query5] = useSuspenseQueries({
queries: [
queryOptions({
queryKey,
queryFn,
}),
queryOptions({
queryKey,
queryFn,
initialData: { field: 'success' },
}),
{
...queryOptions({
queryKey,
queryFn,
}),
initialData: { field: 'success' },
},
{
...queryOptions({
queryKey,
queryFn,
}),
initialData: undefined,
},
{
...queryOptions({
queryKey,
queryFn,
select: (data) => data.field,
}),
},
],
})
expectTypeOf(query1).toEqualTypeOf<
UseSuspenseQueryResult<{ field: string }>
>()
expectTypeOf(query2).toEqualTypeOf<
UseSuspenseQueryResult<{ field: string }>
>()
expectTypeOf(query3).toEqualTypeOf<
UseSuspenseQueryResult<{ field: string }>
>()
expectTypeOf(query4).toEqualTypeOf<
UseSuspenseQueryResult<{ field: string }>
>()
expectTypeOf(query5).toEqualTypeOf<UseSuspenseQueryResult<string>>()
})
})
it('should be used with useQueryClient', () => {
doNotExecute(async () => {
const queryClient = useQueryClient()
queryClient.invalidateQueries(queryOptions({ queryKey, queryFn }))
queryClient.resetQueries(queryOptions({ queryKey, queryFn }))
queryClient.removeQueries(queryOptions({ queryKey, queryFn }))
queryClient.cancelQueries(queryOptions({ queryKey, queryFn }))
queryClient.prefetchQuery(queryOptions({ queryKey, queryFn }))
queryClient.refetchQueries(queryOptions({ queryKey, queryFn }))
expectTypeOf(
await queryClient.fetchQuery(queryOptions({ queryKey, queryFn })),
).toEqualTypeOf<{ field: string }>()
})
})
it('should be used with queryCache', () => {
doNotExecute(() => {
const queryCache = new QueryCache()
queryCache.find({ queryKey: [] })
queryCache.find(queryOptions({ queryKey, queryFn }))
queryCache.find(queryKey)
})
})
})
3 changes: 1 addition & 2 deletions packages/react-query/src/__tests__/useQuery.types.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useQuery } from '../useQuery'
import { doNotExecute } from './utils'

export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
T,
@@ -8,8 +9,6 @@ export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <

export type Expect<T extends true> = T

const doNotExecute = (_func: () => void) => true

describe('initialData', () => {
describe('Config object overload', () => {
it('TData should always be defined when initialData is provided as an object', () => {
102 changes: 102 additions & 0 deletions packages/react-query/src/__tests__/useSuspenseQueries.types.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { expectTypeOf } from 'expect-type'
import { queryOptions } from '..'
import { useSuspenseQueries } from '..'
import { doNotExecute } from './utils'
import type { UseSuspenseQueryResult } from '../useSuspenseQuery'

export const queryKey = ['key'] as const
const sleep = (ms: number) =>
new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms))
export const queryFn = () => sleep(10).then(() => ({ text: 'response' }))
export const select = (data: Awaited<ReturnType<typeof queryFn>>) => data.text

describe('useSuspenseQueries', () => {
it('type check', () => {
doNotExecute(() => {
useSuspenseQueries({
queries: [
{
queryKey: [...queryKey, 1] as const,
queryFn,
// @ts-expect-error no suspense
suspense: false,
},
] as const,
})
useSuspenseQueries({
queries: [
{
queryKey: [...queryKey, 2] as const,
queryFn,
select,
// @ts-expect-error no suspense
suspense: true,
},
] as const,
})
useSuspenseQueries({
queries: [
{
queryKey: [...queryKey, 3] as const,
queryFn,
// @ts-expect-error no enabled
enabled: true,
},
] as const,
})
useSuspenseQueries({
queries: [
{
queryKey: [...queryKey, 4] as const,
queryFn,
// @ts-expect-error no enabled
enabled: true,
select,
},
] as const,
})
useSuspenseQueries({
queries: [
{
queryKey: [...queryKey, 4] as const,
queryFn,
// @ts-expect-error no networkMode
networkMode: 'always',
select,
},
] as const,
})
useSuspenseQueries({
queries: [
queryOptions({
queryKey: [...queryKey, 4] as const,
queryFn: () => Promise.resolve({ field: 'success' }),
select: (data) => data.field,
}),
] as const,
})
// @ts-expect-error if no items
useSuspenseQueries({})
// @ts-expect-error if no items
useSuspenseQueries()

const [query1, query2, query3] = useSuspenseQueries({
queries: [
{ queryKey: [...queryKey, 5] as const, queryFn },
{ queryKey: [...queryKey, 6] as const, queryFn, select },
queryOptions({
queryKey: [...queryKey, 4] as const,
queryFn: () => Promise.resolve({ field: 'success' }),
select: (data) => data.field,
}),
] as const,
})

expectTypeOf(query1).toEqualTypeOf<
UseSuspenseQueryResult<{ text: string }>
>()
expectTypeOf(query2).toEqualTypeOf<UseSuspenseQueryResult<string>>()
expectTypeOf(query3).toEqualTypeOf<UseSuspenseQueryResult<string>>()
})
})
})
81 changes: 81 additions & 0 deletions packages/react-query/src/__tests__/useSuspenseQuery.types.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expectTypeOf } from 'expect-type'
import {
type UseSuspenseQueryResult,
useSuspenseQuery,
} from '../useSuspenseQuery'
import { queryOptions } from '..'
import { doNotExecute } from './utils'

const queryKey = ['key'] as const
const sleep = (ms: number) =>
new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms))
const queryFn = () => sleep(10).then(() => ({ text: 'response' }))

describe('useSuspenseQuery', () => {
it('type check', () => {
doNotExecute(() => {
//@ts-expect-error no arg
useSuspenseQuery()
useSuspenseQuery({
queryKey,
queryFn,
//@ts-expect-error no suspense
suspense: boolean,
})
useSuspenseQuery({
queryKey,
queryFn,
//@ts-expect-error no useErrorBoundary
useErrorBoundary: boolean,
})
useSuspenseQuery({
queryKey,
queryFn,
//@ts-expect-error no enabled
enabled: boolean,
})
useSuspenseQuery({
queryKey,
queryFn,
//@ts-expect-error no placeholderData
placeholderData: 'placeholder',
})
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
useSuspenseQuery({
queryKey,
queryFn,
//@ts-expect-error no isPlaceholderData
}).isPlaceholderData
useSuspenseQuery({
queryKey,
queryFn,
//@ts-expect-error no networkMode
networkMode: 'always',
})

expectTypeOf(useSuspenseQuery({ queryKey, queryFn })).toEqualTypeOf<
UseSuspenseQueryResult<{ text: string }>
>()
expectTypeOf(
useSuspenseQuery({
queryKey,
queryFn,
select: (data) => data.text,
}),
).toEqualTypeOf<UseSuspenseQueryResult<string>>()
const options = queryOptions({
queryKey,
queryFn,
})
expectTypeOf(useSuspenseQuery(options)).toEqualTypeOf<
UseSuspenseQueryResult<{ text: string }>
>()
expectTypeOf(
useSuspenseQuery({
...options,
select: (data) => data.text,
}),
).toEqualTypeOf<UseSuspenseQueryResult<string>>()
})
})
})
2 changes: 2 additions & 0 deletions packages/react-query/src/__tests__/utils.tsx
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ import * as utils from '@tanstack/query-core'
import { QueryClient, QueryClientProvider } from '..'
import type { ContextOptions, MutationOptions, QueryClientConfig } from '..'

export const doNotExecute = (_func: () => void) => true

export function renderWithClient(
client: QueryClient,
ui: React.ReactElement,
3 changes: 3 additions & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ export * from './types'
export { useQueries } from './useQueries'
export type { QueriesResults, QueriesOptions } from './useQueries'
export { useQuery } from './useQuery'
export { useSuspenseQuery } from './useSuspenseQuery'
export { useSuspenseQueries } from './useSuspenseQueries'
export {
defaultContext,
QueryClientProvider,
@@ -29,3 +31,4 @@ export { useIsMutating } from './useIsMutating'
export { useMutation } from './useMutation'
export { useInfiniteQuery } from './useInfiniteQuery'
export { useIsRestoring, IsRestoringProvider } from './isRestoring'
export { queryOptions } from './queryOptions'
86 changes: 86 additions & 0 deletions packages/react-query/src/queryOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type {
InitialDataFunction,
NonUndefinedGuard,
OmitKeyof,
QueryKey,
WithRequired,
} from '@tanstack/query-core'
import type { UseQueryOptions } from './types'

type UseQueryOptionsOmitted<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'onSuccess' | 'onError' | 'onSettled' | 'refetchInterval'
>

type ProhibitedQueryOptionsKeyInV5 = keyof Pick<
UseQueryOptionsOmitted,
'useErrorBoundary' | 'suspense' | 'getNextPageParam' | 'getPreviousPageParam'
>

export type UndefinedInitialDataOptions<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = UseQueryOptionsOmitted<TQueryFnData, TError, TData, TQueryKey> & {
initialData?:
| undefined
| InitialDataFunction<NonUndefinedGuard<TQueryFnData>>
| NonUndefinedGuard<TQueryFnData>
}

export type DefinedInitialDataOptions<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = UseQueryOptionsOmitted<TQueryFnData, TError, TData, TQueryKey> & {
initialData:
| NonUndefinedGuard<TQueryFnData>
| (() => NonUndefinedGuard<TQueryFnData>)
}

export function queryOptions<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: WithRequired<
OmitKeyof<
DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
ProhibitedQueryOptionsKeyInV5
>,
'queryKey'
>,
): WithRequired<
DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey'
>

export function queryOptions<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: WithRequired<
OmitKeyof<
UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
ProhibitedQueryOptionsKeyInV5
>,
'queryKey'
>,
): WithRequired<
UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey'
>

export function queryOptions(options: unknown) {
return options
}
72 changes: 50 additions & 22 deletions packages/react-query/src/useQueries.ts
Original file line number Diff line number Diff line change
@@ -17,8 +17,12 @@ import {
shouldSuspend,
willFetch,
} from './suspense'
import type { QueryFunction, QueryKey } from '@tanstack/query-core'
import type { UseQueryOptions, UseQueryResult } from './types'
import type { OmitKeyof, QueryFunction, QueryKey } from '@tanstack/query-core'
import type {
DefinedUseQueryResult,
UseQueryOptions,
UseQueryResult,
} from './types'

// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
// - `context` is omitted as it is passed as a root-level option to `useQueries` instead.
@@ -27,7 +31,10 @@ type UseQueryOptionsForUseQueries<
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'context'>
> = OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'context'
>

// Avoid TS depth-limit error in case of large array literal
type MAXIMUM_DEPTH = 20
@@ -67,29 +74,46 @@ type GetOptions<T> =
: // Fallback
UseQueryOptionsForUseQueries

type GetResults<T> =
// A defined initialData setting should return a DefinedUseQueryResult rather than UseQueryResult
type GetDefinedOrUndefinedQueryResult<T, TData, TError = unknown> = T extends {
initialData?: infer TInitialData
}
? unknown extends TInitialData
? UseQueryResult<TData, TError>
: TInitialData extends TData
? DefinedUseQueryResult<TData, TError>
: TInitialData extends () => infer TInitialDataResult
? unknown extends TInitialDataResult
? UseQueryResult<TData, TError>
: TInitialDataResult extends TData
? DefinedUseQueryResult<TData, TError>
: UseQueryResult<TData, TError>
: UseQueryResult<TData, TError>
: UseQueryResult<TData, TError>

type GetUseQueryResult<T> =
// Part 1: responsible for mapping explicit type parameter to function result, if object
T extends { queryFnData: any; error?: infer TError; data: infer TData }
? UseQueryResult<TData, TError>
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseQueryResult<TQueryFnData, TError>
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseQueryResult<TData, TError>
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
T extends [any, infer TError, infer TData]
? UseQueryResult<TData, TError>
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
: T extends [infer TQueryFnData, infer TError]
? UseQueryResult<TQueryFnData, TError>
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
: T extends [infer TQueryFnData]
? UseQueryResult<TQueryFnData>
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData>
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
T extends {
queryFn?: QueryFunction<unknown, any>
select: (data: any) => infer TData
}
? UseQueryResult<TData>
? GetDefinedOrUndefinedQueryResult<T, TData>
: T extends { queryFn?: QueryFunction<infer TQueryFnData, any> }
? UseQueryResult<TQueryFnData>
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData>
: // Fallback
UseQueryResult

@@ -98,16 +122,16 @@ type GetResults<T> =
*/
export type QueriesOptions<
T extends any[],
Result extends any[] = [],
Depth extends ReadonlyArray<number> = [],
> = Depth['length'] extends MAXIMUM_DEPTH
TResult extends any[] = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? UseQueryOptionsForUseQueries[]
: T extends []
? []
: T extends [infer Head]
? [...Result, GetOptions<Head>]
? [...TResult, GetOptions<Head>]
: T extends [infer Head, ...infer Tail]
? QueriesOptions<[...Tail], [...Result, GetOptions<Head>], [...Depth, 1]>
? QueriesOptions<[...Tail], [...TResult, GetOptions<Head>], [...TDepth, 1]>
: unknown[] extends T
? T
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
@@ -127,16 +151,20 @@ export type QueriesOptions<
*/
export type QueriesResults<
T extends any[],
Result extends any[] = [],
Depth extends ReadonlyArray<number> = [],
> = Depth['length'] extends MAXIMUM_DEPTH
TResults extends any[] = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? UseQueryResult[]
: T extends []
? []
: T extends [infer Head]
? [...Result, GetResults<Head>]
? [...TResults, GetUseQueryResult<Head>]
: T extends [infer Head, ...infer Tail]
? QueriesResults<[...Tail], [...Result, GetResults<Head>], [...Depth, 1]>
? QueriesResults<
[...Tail],
[...TResults, GetUseQueryResult<Head>],
[...TDepth, 1]
>
: T extends UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
77 changes: 55 additions & 22 deletions packages/react-query/src/useQuery.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,71 @@
'use client'
import { QueryObserver, parseQueryArgs } from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import type { QueryFunction, QueryKey } from '@tanstack/query-core'
import type {
DefinedInitialDataOptions,
UndefinedInitialDataOptions,
} from './queryOptions'
import type {
InitialDataFunction,
NonUndefinedGuard,
OmitKeyof,
QueryFunction,
QueryKey,
} from '@tanstack/query-core'
import type {
DefinedUseQueryResult,
UseQueryOptions,
UseQueryResult,
} from './types'

// HOOK

export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: Omit<
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): DefinedUseQueryResult<TData, TError>
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'initialData'
> & { initialData?: () => undefined },
> & {
initialData:
| NonUndefinedGuard<TQueryFnData>
| (() => NonUndefinedGuard<TQueryFnData>)
},
): DefinedUseQueryResult<TData, TError>
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): UseQueryResult<TData, TError>

export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: Omit<
options: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'initialData'
> & { initialData: TQueryFnData | (() => TQueryFnData) },
): DefinedUseQueryResult<TData, TError>

> & {
initialData?:
| undefined
| InitialDataFunction<NonUndefinedGuard<TQueryFnData>>
| NonUndefinedGuard<TQueryFnData>
},
): UseQueryResult<TData, TError>
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
@@ -43,45 +75,46 @@ export function useQuery<
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): UseQueryResult<TData, TError>

/** @deprecated */
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryKey: TQueryKey,
options?: Omit<
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'initialData'
> & { initialData?: () => undefined },
>,
): UseQueryResult<TData, TError>

/** @deprecated */
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryKey: TQueryKey,
options?: Omit<
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'initialData'
> & { initialData: TQueryFnData | (() => TQueryFnData) },
): DefinedUseQueryResult<TData, TError>

/** @deprecated */
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
queryKey: TQueryKey,
options?: Omit<
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey'
>,
): UseQueryResult<TData, TError>

/** @deprecated */
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
@@ -90,12 +123,12 @@ export function useQuery<
>(
queryKey: TQueryKey,
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
options?: Omit<
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn' | 'initialData'
> & { initialData?: () => undefined },
): UseQueryResult<TData, TError>

/** @deprecated */
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
@@ -104,12 +137,12 @@ export function useQuery<
>(
queryKey: TQueryKey,
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
options?: Omit<
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn' | 'initialData'
> & { initialData: TQueryFnData | (() => TQueryFnData) },
): DefinedUseQueryResult<TData, TError>

/** @deprecated */
export function useQuery<
TQueryFnData = unknown,
TError = unknown,
@@ -118,12 +151,12 @@ export function useQuery<
>(
queryKey: TQueryKey,
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
options?: Omit<
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn'
>,
): UseQueryResult<TData, TError>

/** @deprecated */
export function useQuery<
TQueryFnData,
TError,
162 changes: 162 additions & 0 deletions packages/react-query/src/useSuspenseQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { useQueries } from './useQueries'
import type { UseQueryOptions } from './types'
import type { NetworkMode, QueryFunction } from '@tanstack/query-core'
import type {
UseSuspenseQueryOptions,
UseSuspenseQueryResult,
} from './useSuspenseQuery'

// Avoid TS depth-limit error in case of large array literal
type MAXIMUM_DEPTH = 20

type GetSuspenseOptions<T> =
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
T extends {
queryFnData: infer TQueryFnData
error?: infer TError
data: infer TData
}
? UseSuspenseQueryOptions<TQueryFnData, TError, TData>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseSuspenseQueryOptions<TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseSuspenseQueryOptions<unknown, TError, TData>
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
T extends [infer TQueryFnData, infer TError, infer TData]
? UseSuspenseQueryOptions<TQueryFnData, TError, TData>
: T extends [infer TQueryFnData, infer TError]
? UseSuspenseQueryOptions<TQueryFnData, TError>
: T extends [infer TQueryFnData]
? UseSuspenseQueryOptions<TQueryFnData>
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
T extends {
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey>
select?: (data: any) => infer TData
}
? UseSuspenseQueryOptions<TQueryFnData, unknown, TData, TQueryKey>
: T extends {
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey>
}
? UseSuspenseQueryOptions<TQueryFnData, unknown, TQueryFnData, TQueryKey>
: // Fallback
UseSuspenseQueryOptions

type GetSuspenseResults<T> =
// Part 1: responsible for mapping explicit type parameter to function result, if object
T extends { queryFnData: any; error?: infer TError; data: infer TData }
? UseSuspenseQueryResult<TData, TError>
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
? UseSuspenseQueryResult<TQueryFnData, TError>
: T extends { data: infer TData; error?: infer TError }
? UseSuspenseQueryResult<TData, TError>
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
T extends [any, infer TError, infer TData]
? UseSuspenseQueryResult<TData, TError>
: T extends [infer TQueryFnData, infer TError]
? UseSuspenseQueryResult<TQueryFnData, TError>
: T extends [infer TQueryFnData]
? UseSuspenseQueryResult<TQueryFnData>
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
T extends {
queryFn?: QueryFunction<infer TQueryFnData, any>
select?: (data: any) => infer TData
}
? UseSuspenseQueryResult<unknown extends TData ? TQueryFnData : TData>
: T extends {
queryFn?: QueryFunction<infer TQueryFnData, any>
}
? UseSuspenseQueryResult<TQueryFnData>
: // Fallback
UseSuspenseQueryResult

/**
* SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
*/
export type SuspenseQueriesOptions<
T extends Array<any>,
TResult extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseSuspenseQueryOptions>
: T extends []
? []
: T extends [infer Head]
? [...TResult, GetSuspenseOptions<Head>]
: T extends [infer Head, ...infer Tail]
? SuspenseQueriesOptions<
[...Tail],
[...TResult, GetSuspenseOptions<Head>],
[...TDepth, 1]
>
: Array<unknown> extends T
? T
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
// use this to infer the param types in the case of Array.map() argument
T extends Array<
UseSuspenseQueryOptions<
infer TQueryFnData,
infer TError,
infer TData,
infer TQueryKey
>
>
? Array<UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>>
: // Fallback
Array<UseSuspenseQueryOptions>

/**
* SuspenseQueriesResults reducer recursively maps type param to results
*/
export type SuspenseQueriesResults<
T extends Array<any>,
TResult extends Array<any> = [],
TDepth extends ReadonlyArray<number> = [],
> = TDepth['length'] extends MAXIMUM_DEPTH
? Array<UseSuspenseQueryResult>
: T extends []
? []
: T extends [infer Head]
? [...TResult, GetSuspenseResults<Head>]
: T extends [infer Head, ...infer Tail]
? SuspenseQueriesResults<
[...Tail],
[...TResult, GetSuspenseResults<Head>],
[...TDepth, 1]
>
: T extends Array<
UseSuspenseQueryOptions<
infer TQueryFnData,
infer TError,
infer TData,
any
>
>
? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results
Array<
UseSuspenseQueryResult<
unknown extends TData ? TQueryFnData : TData,
TError
>
>
: // Fallback
Array<UseSuspenseQueryResult>

export function useSuspenseQueries<T extends any[]>({
queries,
context,
}: {
queries: readonly [...SuspenseQueriesOptions<T>]
context?: UseQueryOptions['context']
}): SuspenseQueriesResults<T> {
return useQueries({
queries: queries.map((query) => ({
...query,
enabled: true,
useErrorBoundary: true,
suspense: true,
placeholderData: undefined,
networkMode: 'always' as NetworkMode,
})),
context,
}) as SuspenseQueriesResults<T>
}
58 changes: 58 additions & 0 deletions packages/react-query/src/useSuspenseQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { QueryObserver } from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import type {
DefinedQueryObserverResult,
OmitKeyof,
QueryKey,
} from '@tanstack/query-core'
import type { UseQueryOptions } from './types'

type DistributiveOmit<TObject, TKey extends keyof TObject> = TObject extends any
? Omit<TObject, TKey>
: never

export type UseSuspenseQueryResult<
TData = unknown,
TError = unknown,
> = DistributiveOmit<
DefinedQueryObserverResult<TData, TError>,
'isPlaceholderData'
>

export type UseSuspenseQueryOptions<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
| 'enabled'
| 'useErrorBoundary'
| 'suspense'
| 'placeholderData'
| 'networkMode'
| 'onSuccess'
| 'onError'
| 'onSettled'
| 'getPreviousPageParam'
| 'getNextPageParam'
>

export function useSuspenseQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>) {
return useBaseQuery(
{
...options,
enabled: true,
useErrorBoundary: true,
suspense: true,
placeholderData: undefined,
networkMode: 'always',
},
QueryObserver,
) as UseSuspenseQueryResult<TData, TError>
}
16 changes: 13 additions & 3 deletions packages/vue-query/src/queryCache.ts
Original file line number Diff line number Diff line change
@@ -5,12 +5,22 @@ import type { MaybeRefDeep } from './types'

export class QueryCache extends QC {
find<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
arg1: MaybeRefDeep<QueryKey>,
filters: MaybeRefDeep<QueryFilters>,
): Query<TQueryFnData, TError, TData> | undefined
find<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
queryKey: MaybeRefDeep<QueryKey>,
filters?: MaybeRefDeep<QueryFilters>,
): Query<TQueryFnData, TError, TData> | undefined
find<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
arg1: MaybeRefDeep<QueryKey> | MaybeRefDeep<QueryFilters>,
arg2?: MaybeRefDeep<QueryFilters>,
): Query<TQueryFnData, TError, TData> | undefined {
const arg1Unreffed = cloneDeepUnref(arg1)
const arg1Unreffed = cloneDeepUnref(arg1) as QueryKey | QueryFilters
const arg2Unreffed = cloneDeepUnref(arg2) as QueryFilters
return super.find(arg1Unreffed, arg2Unreffed)
if (isQueryKey(arg1Unreffed)) {
return super.find(arg1Unreffed, arg2Unreffed)
}
return super.find(arg1Unreffed)
}

findAll(
2,037 changes: 1,906 additions & 131 deletions pnpm-lock.yaml

Large diffs are not rendered by default.