diff --git a/.changeset/quiet-cooks-sparkle.md b/.changeset/quiet-cooks-sparkle.md new file mode 100644 index 00000000000..1715d1eaff1 --- /dev/null +++ b/.changeset/quiet-cooks-sparkle.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': minor +--- + +Add framework-agnostic `queryOptions` and `mutationOptions` helpers. diff --git a/packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts b/packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts index 9df834a6b56..c65ed08e9c6 100644 --- a/packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/mutation-options.test-d.ts @@ -1,12 +1,16 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' -import { QueryClient } from '@tanstack/query-core' +import { + QueryClient, + mutationOptions as coreMutationOptions, +} from '@tanstack/query-core' import { injectIsMutating, injectMutation, injectMutationState, mutationOptions, } from '..' +import type { Signal } from '@angular/core' import type { DefaultError, MutationFunctionContext, @@ -174,6 +178,20 @@ describe('mutationOptions', () => { ) }) + it('should infer types when core mutationOptions are used with injectMutation', () => { + const mutation = injectMutation(() => + coreMutationOptions({ + mutationFn: (input: { id: string }) => Promise.resolve(input.id), + }), + ) + + expectTypeOf(mutation.data).toEqualTypeOf>() + expectTypeOf(mutation.variables).toEqualTypeOf< + Signal<{ id: string } | undefined> + >() + expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' }) + }) + it('should infer types when used with injectIsMutating', () => { const key = queryKey() const isMutating = injectIsMutating( diff --git a/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts index c4462afaa34..64d8e1168fb 100644 --- a/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts @@ -2,6 +2,7 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' import { QueryClient, + queryOptions as coreQueryOptions, dataTagSymbol, injectQuery, queryOptions, @@ -69,6 +70,19 @@ it('should work when passed to injectQuery', () => { expectTypeOf(data).toEqualTypeOf>() }) +it('should work when core queryOptions are passed to injectQuery', () => { + const key = queryKey() + const options = coreQueryOptions({ + queryKey: key, + queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry' }), + }) + + const { data } = injectQuery(() => options) + expectTypeOf(data).toEqualTypeOf< + Signal<{ id: string; title: string } | undefined> + >() +}) + it('should work when passed to fetchQuery', () => { const key = queryKey() const options = queryOptions({ diff --git a/packages/lit-query/src/tests/type-inference.test.ts b/packages/lit-query/src/tests/type-inference.test.ts index 6ad3f2a66f3..0910844bc57 100644 --- a/packages/lit-query/src/tests/type-inference.test.ts +++ b/packages/lit-query/src/tests/type-inference.test.ts @@ -1,7 +1,9 @@ import { dataTagErrorSymbol, dataTagSymbol, + mutationOptions as coreMutationOptions, QueryClient, + queryOptions as coreQueryOptions, type DefinedQueryObserverResult, type InfiniteData, type QueryObserverResult, @@ -163,6 +165,18 @@ describe('type inference', () => { { id: number; name: string } | undefined >() + const coreQuery = createQueryController( + host, + () => coreQueryOptions({ + queryKey: ['type-inference', 'core-query'] as const, + queryFn: async () => ({ id: '1', title: 'Do Laundry' }), + }), + client, + ) + expectTypeOf(coreQuery().data).toEqualTypeOf< + { id: string; title: string } | undefined + >() + const mutation = createMutationController( host, mutationOptions({ @@ -175,6 +189,18 @@ describe('type inference', () => { { id: number } | undefined >() + const coreMutation = createMutationController( + host, + () => coreMutationOptions({ + mutationFn: async (input: { id: string }) => input.id, + }), + client, + ) + expectTypeOf(coreMutation().data).toEqualTypeOf() + expectTypeOf(coreMutation().variables).toEqualTypeOf< + { id: string } | undefined + >() + const queryOpts = queryOptions({ queryKey: ['type-inference', 'query-options'] as const, queryFn: async () => ({ id: 2, name: 'Grace' }), diff --git a/packages/preact-query/src/__tests__/mutationOptions.test-d.tsx b/packages/preact-query/src/__tests__/mutationOptions.test-d.tsx index 2241bc67cb5..f237791245a 100644 --- a/packages/preact-query/src/__tests__/mutationOptions.test-d.tsx +++ b/packages/preact-query/src/__tests__/mutationOptions.test-d.tsx @@ -1,4 +1,7 @@ -import { QueryClient } from '@tanstack/query-core' +import { + QueryClient, + mutationOptions as coreMutationOptions, +} from '@tanstack/query-core' import type { DefaultError, MutationFunctionContext, @@ -162,6 +165,20 @@ describe('mutationOptions', () => { ) }) + it('should infer types when core mutationOptions are used with useMutation', () => { + const mutation = useMutation( + coreMutationOptions({ + mutationFn: (input: { id: string }) => Promise.resolve(input.id), + }), + ) + + expectTypeOf(mutation.data).toEqualTypeOf() + expectTypeOf(mutation.variables).toEqualTypeOf< + { id: string } | undefined + >() + expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' }) + }) + it('should infer types when used with useIsMutating', () => { const isMutating = useIsMutating( mutationOptions({ diff --git a/packages/preact-query/src/__tests__/queryOptions.test-d.tsx b/packages/preact-query/src/__tests__/queryOptions.test-d.tsx index 9b0c5183161..a48df95a84a 100644 --- a/packages/preact-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/preact-query/src/__tests__/queryOptions.test-d.tsx @@ -2,6 +2,7 @@ import { QueriesObserver, QueryClient, dataTagSymbol, + queryOptions as coreQueryOptions, skipToken, } from '@tanstack/query-core' import type { @@ -55,6 +56,17 @@ describe('queryOptions', () => { const { data } = useQuery(options) expectTypeOf(data).toEqualTypeOf() }) + it('should work when core queryOptions are passed to useQuery', () => { + const options = coreQueryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry' }), + }) + + const { data } = useQuery(options) + expectTypeOf(data).toEqualTypeOf< + { id: string; title: string } | undefined + >() + }) it('should work when passed to useSuspenseQuery', () => { const options = queryOptions({ queryKey: queryKey(), diff --git a/packages/query-core/src/__tests__/mutationOptions.test-d.tsx b/packages/query-core/src/__tests__/mutationOptions.test-d.tsx new file mode 100644 index 00000000000..1e51728f0d4 --- /dev/null +++ b/packages/query-core/src/__tests__/mutationOptions.test-d.tsx @@ -0,0 +1,47 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { mutationOptions } from '../mutationOptions' +import type { CoreMutationOptions } from '../index' + +describe('mutationOptions', () => { + it('should infer mutation data and variables', () => { + const options = mutationOptions({ + mutationFn: (variables: { id: string }) => Promise.resolve(variables.id), + onSuccess: (data, variables) => { + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(variables).toEqualTypeOf<{ id: string }>() + }, + }) + + expectTypeOf(options.mutationFn) + .parameter(0) + .toEqualTypeOf<{ id: string }>() + type MutationFn = NonNullable + expectTypeOf>>().toEqualTypeOf() + expectTypeOf(options).not.toHaveProperty('mutationKey') + }) + + it('should preserve a required mutationKey', () => { + const options = mutationOptions({ + mutationKey: ['key'], + mutationFn: (variables: { id: string }) => Promise.resolve(variables.id), + }) + + expectTypeOf(options.mutationKey).toEqualTypeOf>() + }) + + it('should export the reusable mutation options type', () => { + expectTypeOf< + CoreMutationOptions + >().toHaveProperty('mutationFn') + }) + + it('should not allow _defaulted', () => { + expectTypeOf(mutationOptions).parameter(0).not.toHaveProperty('_defaulted') + + mutationOptions({ + mutationFn: () => Promise.resolve(5), + // @ts-expect-error _defaulted is an internal option + _defaulted: true, + }) + }) +}) diff --git a/packages/query-core/src/__tests__/queryOptions.test-d.tsx b/packages/query-core/src/__tests__/queryOptions.test-d.tsx new file mode 100644 index 00000000000..084ee7891c5 --- /dev/null +++ b/packages/query-core/src/__tests__/queryOptions.test-d.tsx @@ -0,0 +1,102 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { dataTagErrorSymbol, dataTagSymbol } from '../types' +import { skipToken } from '../utils' +import { QueryClient } from '../queryClient' +import { queryOptions } from '../queryOptions' + +describe('queryOptions', () => { + it('should infer query data from queryFn', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + type QueryFn = Exclude + + expectTypeOf>>().toEqualTypeOf() + expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf() + expectTypeOf(options.queryKey[dataTagErrorSymbol]).toEqualTypeOf() + }) + + it('should tag queryKey with the query data type', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + const queryClient = new QueryClient() + const data = queryClient.getQueryData(options.queryKey) + + expectTypeOf(data).toEqualTypeOf() + }) + + it('should type check values passed to setQueryData from the tagged queryKey', () => { + const options = queryOptions({ + queryKey: ['todo'], + queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry' }), + }) + const queryClient = new QueryClient() + + queryClient.setQueryData(options.queryKey, { + id: '1', + title: 'Wash dishes', + }) + + queryClient.setQueryData(options.queryKey, (prev) => { + expectTypeOf(prev).toEqualTypeOf< + { id: string; title: string } | undefined + >() + return prev + }) + + // @ts-expect-error title should be a string + queryClient.setQueryData(options.queryKey, { id: '1', title: 1 }) + + // @ts-expect-error title is required + queryClient.setQueryData(options.queryKey, { id: '1' }) + }) + + it('should allow initialData without a queryFn', () => { + const options = queryOptions({ + queryKey: ['key'], + initialData: 1, + }) + const queryClient = new QueryClient() + const data = queryClient.getQueryData(options.queryKey) + + expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf() + }) + + it('should infer data when initialData is undefined', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + initialData: undefined, + }) + + expectTypeOf(options.initialData).toEqualTypeOf() + expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf() + expectTypeOf(options.queryKey[dataTagErrorSymbol]).toEqualTypeOf() + }) + + it('should infer a callable queryFn without skipToken', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + enabled: false, + }) + type QueryFn = Exclude + + expectTypeOf().not.toEqualTypeOf() + expectTypeOf>>().toEqualTypeOf() + expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf() + }) + + it('should allow skipToken', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: skipToken, + }) + + expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf() + }) +}) diff --git a/packages/query-core/src/index.ts b/packages/query-core/src/index.ts index 950f0e4c582..010974a36bf 100644 --- a/packages/query-core/src/index.ts +++ b/packages/query-core/src/index.ts @@ -42,6 +42,10 @@ export { export type { MutationFilters, QueryFilters, SkipToken, Updater } from './utils' export { streamedQuery as experimental_streamedQuery } from './streamedQuery' +export { queryOptions } from './queryOptions' +export type { CoreQueryOptions } from './queryOptions' +export { mutationOptions } from './mutationOptions' +export type { CoreMutationOptions } from './mutationOptions' // Types export type { diff --git a/packages/query-core/src/mutationOptions.ts b/packages/query-core/src/mutationOptions.ts new file mode 100644 index 00000000000..a128b26d86f --- /dev/null +++ b/packages/query-core/src/mutationOptions.ts @@ -0,0 +1,57 @@ +import type { + DefaultError, + MutationObserverOptions, + OmitKeyof, + WithRequired, +} from './types' + +export type CoreMutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> = OmitKeyof< + MutationObserverOptions, + '_defaulted' +> + +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: WithRequired< + CoreMutationOptions, + 'mutationKey' + >, +): WithRequired< + CoreMutationOptions, + 'mutationKey' +> + +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: Omit< + CoreMutationOptions, + 'mutationKey' + >, +): Omit< + CoreMutationOptions, + 'mutationKey' +> + +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: CoreMutationOptions, +): CoreMutationOptions { + return options +} diff --git a/packages/query-core/src/queryOptions.ts b/packages/query-core/src/queryOptions.ts new file mode 100644 index 00000000000..5c9d37c5cf8 --- /dev/null +++ b/packages/query-core/src/queryOptions.ts @@ -0,0 +1,113 @@ +import type { + DataTag, + DefaultError, + InitialDataFunction, + NonUndefinedGuard, + OmitKeyof, + QueryFunction, + QueryKey, + QueryObserverOptions, +} from './types' +import type { SkipToken } from './utils' + +export type UndefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey +> & { + initialData?: + | undefined + | InitialDataFunction> + | NonUndefinedGuard +} + +export type UnusedSkipTokenOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = OmitKeyof< + QueryObserverOptions, + 'queryFn' | 'initialData' +> & { + queryFn?: Exclude< + QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + >['queryFn'], + SkipToken | undefined + > + initialData?: undefined +} + +export type DefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit< + QueryObserverOptions, + 'queryFn' +> & { + initialData: + | NonUndefinedGuard + | (() => NonUndefinedGuard) + queryFn?: QueryFunction +} + +export type CoreQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = + | DefinedInitialDataOptions + | UndefinedInitialDataOptions + | UnusedSkipTokenOptions + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: DefinedInitialDataOptions, +): DefinedInitialDataOptions & { + queryKey: DataTag +} + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UnusedSkipTokenOptions, +): UnusedSkipTokenOptions & { + queryKey: DataTag +} + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UndefinedInitialDataOptions, +): UndefinedInitialDataOptions & { + queryKey: DataTag +} + +export function queryOptions(options: unknown) { + return options +} diff --git a/packages/react-query/src/__tests__/mutationOptions.test-d.tsx b/packages/react-query/src/__tests__/mutationOptions.test-d.tsx index 43391f2bd85..f3efc02ff41 100644 --- a/packages/react-query/src/__tests__/mutationOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/mutationOptions.test-d.tsx @@ -1,5 +1,8 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' -import { QueryClient } from '@tanstack/query-core' +import { + QueryClient, + mutationOptions as coreMutationOptions, +} from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { useIsMutating, useMutation, useMutationState } from '..' import { mutationOptions } from '../mutationOptions' @@ -161,6 +164,20 @@ describe('mutationOptions', () => { ) }) + it('should infer types when core mutationOptions are used with useMutation', () => { + const mutation = useMutation( + coreMutationOptions({ + mutationFn: (input: { id: string }) => Promise.resolve(input.id), + }), + ) + + expectTypeOf(mutation.data).toEqualTypeOf() + expectTypeOf(mutation.variables).toEqualTypeOf< + { id: string } | undefined + >() + expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' }) + }) + it('should infer types when used with useIsMutating', () => { const isMutating = useIsMutating( mutationOptions({ diff --git a/packages/react-query/src/__tests__/queryOptions.test-d.tsx b/packages/react-query/src/__tests__/queryOptions.test-d.tsx index fb67f069061..d32567c1d8d 100644 --- a/packages/react-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/queryOptions.test-d.tsx @@ -2,6 +2,7 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueriesObserver, QueryClient, + queryOptions as coreQueryOptions, dataTagSymbol, skipToken, } from '@tanstack/query-core' @@ -55,6 +56,17 @@ describe('queryOptions', () => { const { data } = useQuery(options) expectTypeOf(data).toEqualTypeOf() }) + it('should work when core queryOptions are passed to useQuery', () => { + const options = coreQueryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry' }), + }) + + const { data } = useQuery(options) + expectTypeOf(data).toEqualTypeOf< + { id: string; title: string } | undefined + >() + }) it('should work when passed to useSuspenseQuery', () => { const options = queryOptions({ queryKey: queryKey(), diff --git a/packages/solid-query/src/__tests__/mutationOptions.test-d.tsx b/packages/solid-query/src/__tests__/mutationOptions.test-d.tsx index 972296f5ae5..c7f2274a3b5 100644 --- a/packages/solid-query/src/__tests__/mutationOptions.test-d.tsx +++ b/packages/solid-query/src/__tests__/mutationOptions.test-d.tsx @@ -1,5 +1,8 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' -import { QueryClient } from '@tanstack/query-core' +import { + QueryClient, + mutationOptions as coreMutationOptions, +} from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { useIsMutating, useMutation, useMutationState } from '..' import { mutationOptions } from '../mutationOptions' @@ -158,6 +161,20 @@ describe('mutationOptions', () => { ) }) + it('should infer types when core mutationOptions are used with useMutation', () => { + const mutation = useMutation(() => + coreMutationOptions({ + mutationFn: (input: { id: string }) => Promise.resolve(input.id), + }), + ) + + expectTypeOf(mutation.data).toEqualTypeOf() + expectTypeOf(mutation.variables).toEqualTypeOf< + { id: string } | undefined + >() + expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' }) + }) + it('should infer types when used with useIsMutating', () => { const isMutating = useIsMutating(() => mutationOptions({ diff --git a/packages/solid-query/src/__tests__/queryOptions.test-d.tsx b/packages/solid-query/src/__tests__/queryOptions.test-d.tsx index 05672ba10cb..331372ef3a3 100644 --- a/packages/solid-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/solid-query/src/__tests__/queryOptions.test-d.tsx @@ -1,6 +1,7 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { QueryClient, + queryOptions as coreQueryOptions, dataTagErrorSymbol, dataTagSymbol, skipToken, @@ -84,6 +85,17 @@ describe('queryOptions', () => { const data = await new QueryClient().query(options) expectTypeOf(data).toEqualTypeOf() }) + it('should work when core queryOptions are passed to useQuery', () => { + const options = coreQueryOptions({ + queryKey: queryKey(), + queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry' }), + }) + + const { data } = useQuery(() => options) + expectTypeOf(data).toEqualTypeOf< + { id: string; title: string } | undefined + >() + }) it('should work when passed to fetchQuery', async () => { const options = queryOptions({ queryKey: queryKey(), diff --git a/packages/svelte-query/tests/createQuery/createQuery.test-d.ts b/packages/svelte-query/tests/createQuery/createQuery.test-d.ts index 8b6590b3347..a3836b4ff3b 100644 --- a/packages/svelte-query/tests/createQuery/createQuery.test-d.ts +++ b/packages/svelte-query/tests/createQuery/createQuery.test-d.ts @@ -1,4 +1,5 @@ import { describe, expectTypeOf, it } from 'vitest' +import { queryOptions as coreQueryOptions } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { createQuery, queryOptions } from '../../src/index.js' @@ -28,6 +29,19 @@ describe('createQuery', () => { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) + it('TData should be inferred when passed through core queryOptions', () => { + const key = queryKey() + const options = coreQueryOptions({ + queryKey: key, + queryFn: () => ({ id: '1', title: 'Do Laundry' }), + }) + const { data } = createQuery(() => options) + + expectTypeOf(data).toEqualTypeOf< + { id: string; title: string } | undefined + >() + }) + it('TData should have undefined in the union when initialData is NOT provided', () => { const key = queryKey() const { data } = createQuery(() => ({ diff --git a/packages/svelte-query/tests/mutationOptions/mutationOptions.test-d.ts b/packages/svelte-query/tests/mutationOptions/mutationOptions.test-d.ts index 00e657db61c..a4a2b9cec1e 100644 --- a/packages/svelte-query/tests/mutationOptions/mutationOptions.test-d.ts +++ b/packages/svelte-query/tests/mutationOptions/mutationOptions.test-d.ts @@ -1,5 +1,8 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' -import { QueryClient } from '@tanstack/query-core' +import { + QueryClient, + mutationOptions as coreMutationOptions, +} from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { createMutation, @@ -184,6 +187,20 @@ describe('mutationOptions', () => { ) }) + it('should work when core mutationOptions are used with createMutation', () => { + const mutation = createMutation(() => + coreMutationOptions({ + mutationFn: (input: { id: string }) => Promise.resolve(input.id), + }), + ) + + expectTypeOf(mutation.data).toEqualTypeOf() + expectTypeOf(mutation.variables).toEqualTypeOf< + { id: string } | undefined + >() + expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' }) + }) + it('should work when used with useIsMutating', () => { const key = queryKey() diff --git a/packages/vue-query/src/__tests__/mutationOptions.test-d.ts b/packages/vue-query/src/__tests__/mutationOptions.test-d.ts index 14d19a9f43e..c94fe5b019f 100644 --- a/packages/vue-query/src/__tests__/mutationOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/mutationOptions.test-d.ts @@ -1,5 +1,8 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' -import { QueryClient } from '@tanstack/query-core' +import { + QueryClient, + mutationOptions as coreMutationOptions, +} from '@tanstack/query-core' import { useMutation } from '../useMutation' import { useIsMutating, useMutationState } from '../useMutationState' import { mutationOptions } from '../mutationOptions' @@ -163,6 +166,19 @@ describe('mutationOptions', () => { ) }) + it('should work when core mutationOptions are used with useMutation', () => { + const mutation = useMutation( + () => coreMutationOptions({ + mutationFn: (input: { id: string }) => Promise.resolve(input.id), + }), + ) + + expectTypeOf(mutation.data.value).toEqualTypeOf() + expectTypeOf(mutation.variables.value).toEqualTypeOf< + { id: string } | undefined + >() + }) + it('should work when used with useIsMutating', () => { const isMutating = useIsMutating( mutationOptions({ diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index 8b1ee84c1e6..2927267407a 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -1,6 +1,9 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { computed, reactive, ref } from 'vue-demi' -import { dataTagSymbol } from '@tanstack/query-core' +import { + queryOptions as coreQueryOptions, + dataTagSymbol, +} from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { QueryClient } from '../queryClient' import { queryOptions } from '../queryOptions' @@ -69,6 +72,37 @@ describe('queryOptions', () => { const data = await new QueryClient().query(options) expectTypeOf(data).toEqualTypeOf() }) + it('should work when core queryOptions are returned from a useQuery getter', () => { + const key = queryKey() + + const { data } = reactive( + useQuery(() => + coreQueryOptions({ + queryKey: key, + queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry' }), + staleTime: (query) => (query.state.data ? 1000 : 0), + }), + ), + ) + expectTypeOf(data).toEqualTypeOf< + { id: string; title: string } | undefined + >() + }) + it('should not support passing core queryOptions directly to useQuery', () => { + const key = queryKey() + const options = coreQueryOptions({ + queryKey: key, + queryFn: () => Promise.resolve({ id: '1', title: 'Do Laundry' }), + staleTime: (query) => (query.state.data ? 1000 : 0), + }) + expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf<{ + id: string + title: string + }>() + + // @ts-expect-error core queryOptions must be wrapped in a getter + useQuery(options) + }) it('should tag the queryKey with the result type of the QueryFn', () => { const key = queryKey() const { queryKey: tagged } = queryOptions({ diff --git a/packages/vue-query/src/__tests__/useQuery.test.ts b/packages/vue-query/src/__tests__/useQuery.test.ts index 567baa03f82..5172fa58996 100644 --- a/packages/vue-query/src/__tests__/useQuery.test.ts +++ b/packages/vue-query/src/__tests__/useQuery.test.ts @@ -6,7 +6,11 @@ import { reactive, ref, } from 'vue-demi' -import { QueryObserver, experimental_streamedQuery } from '@tanstack/query-core' +import { + QueryObserver, + queryOptions as coreQueryOptions, + experimental_streamedQuery, +} from '@tanstack/query-core' import { queryKey, sleep } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { useBaseQuery } from '../useBaseQuery' @@ -99,6 +103,42 @@ describe('useQuery', () => { }) }) + it('should work with core queryOptions getter and be reactive', async () => { + const key = queryKey() + const keyRef = ref('core-key-1') + const resultRef = ref('core-result-1') + const query = useQuery(() => + coreQueryOptions({ + queryKey: [...key, keyRef.value], + queryFn: () => sleep(0).then(() => resultRef.value), + }), + ) + + await vi.advanceTimersByTimeAsync(0) + + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: 'core-result-1' }, + isPending: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isSuccess: { value: true }, + }) + + resultRef.value = 'core-result-2' + keyRef.value = 'core-key-2' + await vi.advanceTimersByTimeAsync(0) + + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: 'core-result-2' }, + isPending: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isSuccess: { value: true }, + }) + }) + it('should return pending status initially', () => { const key = queryKey() const query = useQuery({ diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts index f5c444b3ae9..2fdac4cf31a 100644 --- a/packages/vue-query/src/useBaseQuery.ts +++ b/packages/vue-query/src/useBaseQuery.ts @@ -14,6 +14,7 @@ import { useQueryClient } from './useQueryClient' import { cloneDeepUnref, updateState } from './utils' import type { Ref } from 'vue-demi' import type { + CoreQueryOptions, DefaultedQueryObserverOptions, QueryKey, QueryObserver, @@ -59,16 +60,18 @@ export function useBaseQuery< TPageParam, >( Observer: typeof QueryObserver, - options: MaybeRefOrGetter< - UseQueryOptionsGeneric< - TQueryFnData, - TError, - TData, - TQueryData, - TQueryKey, - TPageParam - > - >, + options: + | MaybeRefOrGetter< + UseQueryOptionsGeneric< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + > + > + | (() => CoreQueryOptions), queryClient?: QueryClient, ): UseBaseQueryReturnType { if (process.env.NODE_ENV === 'development') { @@ -82,10 +85,8 @@ export function useBaseQuery< const client = queryClient || useQueryClient() const defaultedOptions = computed(() => { - let resolvedOptions = options - if (typeof resolvedOptions === 'function') { - resolvedOptions = resolvedOptions() - } + const resolvedOptions = + typeof options === 'function' ? options() : options const clonedOptions = cloneDeepUnref(resolvedOptions as any) if (typeof clonedOptions.enabled === 'function') { diff --git a/packages/vue-query/src/useQuery.ts b/packages/vue-query/src/useQuery.ts index bab7f7119e7..b668b41920d 100644 --- a/packages/vue-query/src/useQuery.ts +++ b/packages/vue-query/src/useQuery.ts @@ -1,6 +1,7 @@ import { QueryObserver } from '@tanstack/query-core' import { useBaseQuery } from './useBaseQuery' import type { + CoreQueryOptions, DefaultError, DefinedQueryObserverResult, InitialDataFunction, @@ -98,6 +99,37 @@ export type UseQueryDefinedReturnType = UseBaseQueryReturnType< DefinedQueryObserverResult > +type DefinedCoreQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey extends QueryKey, +> = CoreQueryOptions & { + initialData: + | NonUndefinedGuard + | (() => NonUndefinedGuard) +} + +export function useQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: () => DefinedCoreQueryOptions, + queryClient?: QueryClient, +): UseQueryDefinedReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: () => CoreQueryOptions, + queryClient?: QueryClient, +): UseQueryReturnType + export function useQuery< TQueryFnData = unknown, TError = DefaultError, @@ -136,9 +168,11 @@ export function useQuery< TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( - options: MaybeRefOrGetter< - UseQueryOptions - >, + options: + | MaybeRefOrGetter< + UseQueryOptions + > + | (() => CoreQueryOptions), queryClient?: QueryClient, ): | UseQueryReturnType