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 89691bf81a4..ea2dc99ce50 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 8642cf07d72..243b1f7e486 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 @@ -1,11 +1,18 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' -import { QueryClient, dataTagSymbol, injectQuery, queryOptions } from '..' +import { + QueryClient, + queryOptions as coreQueryOptions, + dataTagSymbol, +} from '@tanstack/query-core' +import { injectQuery, queryOptions } from '..' import type { Signal } from '@angular/core' describe('queryOptions', () => { it('should not allow excess properties', () => { - expectTypeOf(queryOptions).parameter(0).not.toHaveProperty('stallTime') + expectTypeOf(queryOptions) + .parameter(0) + .not.toHaveProperty('stallTime') }) it('should infer types for callbacks', () => { @@ -34,9 +41,9 @@ describe('queryOptions', () => { !id ? undefined : { - id, - title: 'Initial Data', - }, + id, + title: 'Initial Data', + }, }) expectTypeOf(options(null).initialData).returns.toEqualTypeOf< @@ -56,6 +63,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 e8c6c3c525d..1b046d33d6d 100644 --- a/packages/lit-query/src/tests/type-inference.test.ts +++ b/packages/lit-query/src/tests/type-inference.test.ts @@ -1,6 +1,8 @@ import { dataTagSymbol, + mutationOptions as coreMutationOptions, QueryClient, + queryOptions as coreQueryOptions, type DefinedQueryObserverResult, type QueryObserverResult, } from '@tanstack/query-core' @@ -161,6 +163,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({ @@ -173,6 +187,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 86f48506a53..8b57d2ff535 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 6f0d34c6bac..47542bf9373 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 { @@ -48,6 +49,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 a4267aabc97..cb825f1c22e 100644 --- a/packages/query-core/src/index.ts +++ b/packages/query-core/src/index.ts @@ -41,6 +41,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 5163fb210cb..5fe71b93c74 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 14f92a8da3e..e817abd25b7 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' @@ -48,6 +49,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 58cb139bd98..6eb19cbcaf9 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 299536290e6..576f84db9f4 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, @@ -39,6 +40,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 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 a3e8c6d0d41..68b02176bfc 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' @@ -39,6 +42,37 @@ describe('queryOptions', () => { const { data } = reactive(useQuery(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 d794c97c2b1..ddc04971bc6 100644 --- a/packages/vue-query/src/__tests__/useQuery.test.ts +++ b/packages/vue-query/src/__tests__/useQuery.test.ts @@ -6,7 +6,10 @@ import { reactive, ref, } from 'vue-demi' -import { QueryObserver } from '@tanstack/query-core' +import { + QueryObserver, + queryOptions as coreQueryOptions, +} from '@tanstack/query-core' import { queryKey, sleep } from '@tanstack/query-test-utils' import { useQuery } from '../useQuery' import { useBaseQuery } from '../useBaseQuery' @@ -98,6 +101,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 116e91baefe..9920e19b59f 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, @@ -88,6 +89,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, @@ -126,9 +158,11 @@ export function useQuery< TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( - options: MaybeRefOrGetter< - UseQueryOptions - >, + options: + | MaybeRefOrGetter< + UseQueryOptions + > + | (() => CoreQueryOptions), queryClient?: QueryClient, ): | UseQueryReturnType