From 2ec89c6ba05e23af173a5e67f507036717af9ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B3=E3=82=B3=E3=83=AD?= <4946624+shincurry@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:42:23 +0000 Subject: [PATCH 1/8] Add mutationOptions for vue-query --- packages/vue-query/src/index.ts | 1 + packages/vue-query/src/mutationOptions.ts | 63 +++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 packages/vue-query/src/mutationOptions.ts diff --git a/packages/vue-query/src/index.ts b/packages/vue-query/src/index.ts index 5ea6e26f83..5ccee15f15 100644 --- a/packages/vue-query/src/index.ts +++ b/packages/vue-query/src/index.ts @@ -16,6 +16,7 @@ export { useQuery } from './useQuery' export { useQueries } from './useQueries' export { useInfiniteQuery } from './useInfiniteQuery' export { useMutation } from './useMutation' +export { mutationOptions } from './mutationOptions' export { useIsFetching } from './useIsFetching' export { useIsMutating, useMutationState } from './useMutationState' export { VUE_QUERY_CLIENT } from './utils' diff --git a/packages/vue-query/src/mutationOptions.ts b/packages/vue-query/src/mutationOptions.ts new file mode 100644 index 0000000000..f89bd9f48a --- /dev/null +++ b/packages/vue-query/src/mutationOptions.ts @@ -0,0 +1,63 @@ +import type { DefaultError, MutationObserverOptions, WithRequired } from '@tanstack/query-core' +import type { UseMutationOptions } from './useMutation' +import type { MaybeRefDeep, ShallowOption } from './types' + +type UseMutationOptionsBase = + MutationObserverOptions & + ShallowOption + +type MutationOptionsWithMutationKeyRequired = + | MaybeRefDeep< + WithRequired< + UseMutationOptionsBase, + 'mutationKey' + > + > + | (() => MaybeRefDeep< + WithRequired< + UseMutationOptionsBase, + 'mutationKey' + > + >) + +type MutationOptionsWithMutationKeyOmit = + | MaybeRefDeep< + Omit< + UseMutationOptionsBase, + 'mutationKey' + > + > + | (() => MaybeRefDeep< + Omit< + UseMutationOptionsBase, + 'mutationKey' + > + >) + + +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: MutationOptionsWithMutationKeyRequired, +): MutationOptionsWithMutationKeyRequired +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: MutationOptionsWithMutationKeyOmit, +): MutationOptionsWithMutationKeyOmit +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: UseMutationOptions, +): UseMutationOptions { + return options +} From 8007eebe55ba17eefeb62fa60a4d1b3a8a04c951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B3=E3=82=B3=E3=83=AD?= <4946624+shincurry@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:56:17 +0000 Subject: [PATCH 2/8] new type helper utils: DeepUnwrapRefOrGetter, MaybeRefDeepOrGetter --- packages/vue-query/src/mutationOptions.ts | 67 +++++++++-------------- packages/vue-query/src/types.ts | 6 ++ packages/vue-query/src/useMutation.ts | 10 +--- 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/packages/vue-query/src/mutationOptions.ts b/packages/vue-query/src/mutationOptions.ts index f89bd9f48a..98dd12c01c 100644 --- a/packages/vue-query/src/mutationOptions.ts +++ b/packages/vue-query/src/mutationOptions.ts @@ -1,39 +1,6 @@ -import type { DefaultError, MutationObserverOptions, WithRequired } from '@tanstack/query-core' -import type { UseMutationOptions } from './useMutation' -import type { MaybeRefDeep, ShallowOption } from './types' - -type UseMutationOptionsBase = - MutationObserverOptions & - ShallowOption - -type MutationOptionsWithMutationKeyRequired = - | MaybeRefDeep< - WithRequired< - UseMutationOptionsBase, - 'mutationKey' - > - > - | (() => MaybeRefDeep< - WithRequired< - UseMutationOptionsBase, - 'mutationKey' - > - >) - -type MutationOptionsWithMutationKeyOmit = - | MaybeRefDeep< - Omit< - UseMutationOptionsBase, - 'mutationKey' - > - > - | (() => MaybeRefDeep< - Omit< - UseMutationOptionsBase, - 'mutationKey' - > - >) - +import type { UseMutationOptions } from './useMutation'; +import type { DefaultError, WithRequired } from '@tanstack/query-core' +import type { DeepUnwrapRefOrGetter, MaybeRefDeepOrGetter } from './types' export function mutationOptions< TData = unknown, @@ -41,16 +8,36 @@ export function mutationOptions< TVariables = void, TOnMutateResult = unknown, >( - options: MutationOptionsWithMutationKeyRequired, -): MutationOptionsWithMutationKeyRequired + options: MaybeRefDeepOrGetter< + WithRequired< + DeepUnwrapRefOrGetter>, + 'mutationKey' + > + >, +): MaybeRefDeepOrGetter< + WithRequired< + DeepUnwrapRefOrGetter>, + 'mutationKey' + > +> export function mutationOptions< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, >( - options: MutationOptionsWithMutationKeyOmit, -): MutationOptionsWithMutationKeyOmit + options: MaybeRefDeepOrGetter< + Omit< + DeepUnwrapRefOrGetter>, + 'mutationKey' + > + >, +): MaybeRefDeepOrGetter< + Omit< + DeepUnwrapRefOrGetter>, + 'mutationKey' + > +> export function mutationOptions< TData = unknown, TError = DefaultError, diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts index eb793c811e..46ff03df2a 100644 --- a/packages/vue-query/src/types.ts +++ b/packages/vue-query/src/types.ts @@ -36,6 +36,8 @@ export type MaybeRefDeep = MaybeRef< : T > +export type MaybeRefDeepOrGetter = MaybeRefDeep | (() => T) + export type NoUnknown = Equal extends true ? never : T export type Equal = @@ -55,6 +57,10 @@ export type DeepUnwrapRef = T extends UnwrapLeaf } : UnwrapRef +export type DeepUnwrapRefOrGetter = T extends (...args: any) => MaybeRefDeep + ? DeepUnwrapRef> + : DeepUnwrapRef + export type ShallowOption = { /** * Return data in a shallow ref object (it is `false` by default). It can be set to `true` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive. diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts index 2bb852acbf..39ccae9c8e 100644 --- a/packages/vue-query/src/useMutation.ts +++ b/packages/vue-query/src/useMutation.ts @@ -21,7 +21,7 @@ import type { MutationObserverOptions, MutationObserverResult, } from '@tanstack/query-core' -import type { MaybeRefDeep, ShallowOption } from './types' +import type { MaybeRefDeepOrGetter, ShallowOption } from './types' import type { QueryClient } from './queryClient' type MutationResult = @@ -39,13 +39,7 @@ export type UseMutationOptions< TError = DefaultError, TVariables = void, TOnMutateResult = unknown, -> = - | MaybeRefDeep< - UseMutationOptionsBase - > - | (() => MaybeRefDeep< - UseMutationOptionsBase - >) +> = MaybeRefDeepOrGetter> type MutateSyncFunction< TData = unknown, From d3e12da4733a09aced921682be1259ade1dd402d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B3=E3=82=B3=E3=83=AD?= <4946624+shincurry@users.noreply.github.com> Date: Wed, 14 Jan 2026 06:56:49 +0000 Subject: [PATCH 3/8] Add tests of mutationOptions --- .../src/__tests__/mutationOptions.test-d.ts | 164 +++++++ .../src/__tests__/mutationOptions.test.ts | 402 ++++++++++++++++++ 2 files changed, 566 insertions(+) create mode 100644 packages/vue-query/src/__tests__/mutationOptions.test-d.ts create mode 100644 packages/vue-query/src/__tests__/mutationOptions.test.ts diff --git a/packages/vue-query/src/__tests__/mutationOptions.test-d.ts b/packages/vue-query/src/__tests__/mutationOptions.test-d.ts new file mode 100644 index 0000000000..ed2c06e9c0 --- /dev/null +++ b/packages/vue-query/src/__tests__/mutationOptions.test-d.ts @@ -0,0 +1,164 @@ +import { assertType, describe, expectTypeOf, it } from 'vitest' +import { reactive, ref } from 'vue-demi' +import { useIsMutating, useMutationState } from '../useMutationState' +import { useMutation } from '../useMutation' +import { mutationOptions } from '../mutationOptions' +import type { + DefaultError, + MutationFunctionContext, + MutationState, +} from '@tanstack/query-core' + +describe('mutationOptions', () => { + it('should not allow excess properties', () => { + // @ts-expect-error this is a good error, because onMutates does not exist! + mutationOptions({ + mutationFn: () => Promise.resolve(5), + mutationKey: ['key'], + onMutates: 1000, + onSuccess: (data) => { + expectTypeOf(data).toEqualTypeOf() + }, + }) + }) + + it('should infer types for callbacks', () => { + mutationOptions({ + mutationFn: () => Promise.resolve(5), + mutationKey: ['key'], + onSuccess: (data) => { + expectTypeOf(data).toEqualTypeOf() + }, + }) + }) + + it('should infer types for onError callback', () => { + mutationOptions({ + mutationFn: () => { + throw new Error('fail') + }, + mutationKey: ['key'], + onError: (error) => { + expectTypeOf(error).toEqualTypeOf() + }, + }) + }) + + it('should infer types for variables', () => { + mutationOptions({ + mutationFn: (vars) => { + expectTypeOf(vars).toEqualTypeOf<{ id: string }>() + return Promise.resolve(5) + }, + mutationKey: ['with-vars'], + }) + }) + + it('should infer result type correctly', () => { + mutationOptions({ + mutationFn: () => Promise.resolve(5), + mutationKey: ['key'], + onMutate: () => { + return { name: 'onMutateResult' } + }, + onSuccess: (_data, _variables, onMutateResult) => { + expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>() + }, + }) + }) + + it('should infer context type correctly', () => { + mutationOptions({ + mutationFn: (_variables, context) => { + expectTypeOf(context).toEqualTypeOf() + return Promise.resolve(5) + }, + mutationKey: ['key'], + onMutate: (_variables, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onSuccess: (_data, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onError: (_error, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + onSettled: (_data, _error, _variables, _onMutateResult, context) => { + expectTypeOf(context).toEqualTypeOf() + }, + }) + }) + + it('should error if mutationFn return type mismatches TData', () => { + assertType( + mutationOptions({ + // @ts-expect-error this is a good error, because return type is string, not number + mutationFn: async () => Promise.resolve('wrong return'), + }), + ) + }) + + it('should allow mutationKey to be omitted', () => { + return mutationOptions({ + mutationFn: () => Promise.resolve(123), + onSuccess: (data) => { + expectTypeOf(data).toEqualTypeOf() + }, + }) + }) + + it('should infer types when used with useMutation', () => { + const mutation = reactive( + useMutation( + mutationOptions({ + mutationKey: ['key'], + mutationFn: () => Promise.resolve('data'), + onSuccess: (data) => { + expectTypeOf(data).toEqualTypeOf() + }, + }), + ), + ) + expectTypeOf(mutation.data).toEqualTypeOf() + + reactive( + useMutation( + // should allow when used with useMutation without mutationKey + mutationOptions({ + mutationFn: () => Promise.resolve('data'), + onSuccess: (data) => { + expectTypeOf(data).toEqualTypeOf() + }, + }), + ), + ) + }) + + it('should infer types when used with useIsMutating', () => { + const isMutating = useIsMutating({ + mutationKey: ['key'], + }) + expectTypeOf(isMutating.value).toEqualTypeOf() + }) + + it('should infer types when used with useMutationState', () => { + const mutationState = useMutationState({ + filters: { + mutationKey: ['key'], + }, + }) + expectTypeOf(mutationState.value).toEqualTypeOf< + Array> + >() + }) + + it('should allow to be passed to useMutation while containing ref in mutationKey', () => { + const options = mutationOptions({ + mutationKey: ['key', ref(1), { nested: ref(2) }], + mutationFn: () => Promise.resolve(5), + }) + + const mutation = reactive(useMutation(options)) + expectTypeOf(mutation.data).toEqualTypeOf() + }) +}) diff --git a/packages/vue-query/src/__tests__/mutationOptions.test.ts b/packages/vue-query/src/__tests__/mutationOptions.test.ts new file mode 100644 index 0000000000..74ce8c90c5 --- /dev/null +++ b/packages/vue-query/src/__tests__/mutationOptions.test.ts @@ -0,0 +1,402 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { onScopeDispose } from 'vue-demi' +import { sleep } from '@tanstack/query-test-utils' +import { mutationOptions } from '../mutationOptions' +import { useMutation } from '../useMutation' +import { useIsMutating, useMutationState } from '../useMutationState' +import { useQueryClient } from '../useQueryClient' +import type { MockedFunction } from 'vitest' +import type { MutationState } from '@tanstack/query-core' + +vi.mock('../useQueryClient') + +describe('mutationOptions', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => { + const object = { + mutationKey: ['key'], + mutationFn: () => sleep(10).then(() => 5), + } as const + + expect(mutationOptions(object)).toStrictEqual(object) + }) + + it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => { + const object = { + mutationFn: () => sleep(10).then(() => 5), + } as const + + expect(mutationOptions(object)).toStrictEqual(object) + }) + + it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => { + const isMutatingArray: Array = [] + const mutationOpts = mutationOptions({ + mutationKey: ['key'], + mutationFn: () => sleep(50).then(() => 'data'), + }) + + const { mutate } = useMutation(mutationOpts) + const isMutating = useIsMutating() + + isMutatingArray.push(isMutating.value) + + mutate() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(isMutating.value) + + await vi.advanceTimersByTimeAsync(51) + isMutatingArray.push(isMutating.value) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(1) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => { + const isMutatingArray: Array = [] + const mutationOpts = mutationOptions({ + mutationFn: () => sleep(50).then(() => 'data'), + }) + + const { mutate } = useMutation(mutationOpts) + const isMutating = useIsMutating() + + isMutatingArray.push(isMutating.value) + + mutate() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(isMutating.value) + + await vi.advanceTimersByTimeAsync(51) + isMutatingArray.push(isMutating.value) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(1) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with useIsMutating', async () => { + const isMutatingArray: Array = [] + const mutationOpts1 = mutationOptions({ + mutationKey: ['key'], + mutationFn: () => sleep(50).then(() => 'data1'), + }) + const mutationOpts2 = mutationOptions({ + mutationFn: () => sleep(50).then(() => 'data2'), + }) + + const { mutate: mutate1 } = useMutation(mutationOpts1) + const { mutate: mutate2 } = useMutation(mutationOpts2) + const isMutating = useIsMutating() + + isMutatingArray.push(isMutating.value) + + mutate1() + mutate2() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(isMutating.value) + + await vi.advanceTimersByTimeAsync(51) + isMutatingArray.push(isMutating.value) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(2) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => { + const isMutatingArray: Array = [] + const mutationKey1 = ['key'] as const + const mutationOpts1 = mutationOptions({ + mutationKey: mutationKey1, + mutationFn: () => sleep(50).then(() => 'data1'), + }) + const mutationOpts2 = mutationOptions({ + mutationFn: () => sleep(50).then(() => 'data2'), + }) + + const { mutate: mutate1 } = useMutation(mutationOpts1) + const { mutate: mutate2 } = useMutation(mutationOpts2) + const isMutating = useIsMutating({ + mutationKey: mutationKey1, + }) + + isMutatingArray.push(isMutating.value) + + mutate1() + mutate2() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(isMutating.value) + + await vi.advanceTimersByTimeAsync(51) + isMutatingArray.push(isMutating.value) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(1) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => { + const isMutatingArray: Array = [] + const queryClient = useQueryClient() + const mutationKey = ['mutation'] as const + const mutationOpts = mutationOptions({ + mutationKey, + mutationFn: () => sleep(500).then(() => 'data'), + }) + + const { mutate } = useMutation(mutationOpts) + isMutatingArray.push(queryClient.isMutating({ mutationKey })) + + mutate() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(queryClient.isMutating({ mutationKey })) + + await vi.advanceTimersByTimeAsync(501) + isMutatingArray.push(queryClient.isMutating({ mutationKey })) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(1) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => { + const isMutatingArray: Array = [] + const queryClient = useQueryClient() + const mutationOpts = mutationOptions({ + mutationFn: () => sleep(500).then(() => 'data'), + }) + + const { mutate } = useMutation(mutationOpts) + isMutatingArray.push(queryClient.isMutating()) + + mutate() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(queryClient.isMutating()) + + await vi.advanceTimersByTimeAsync(501) + isMutatingArray.push(queryClient.isMutating()) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(1) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with queryClient.isMutating', async () => { + const isMutatingArray: Array = [] + const queryClient = useQueryClient() + const mutationOpts1 = mutationOptions({ + mutationKey: ['mutation'], + mutationFn: () => sleep(500).then(() => 'data1'), + }) + const mutationOpts2 = mutationOptions({ + mutationFn: () => sleep(500).then(() => 'data2'), + }) + + const { mutate: mutate1 } = useMutation(mutationOpts1) + const { mutate: mutate2 } = useMutation(mutationOpts2) + isMutatingArray.push(queryClient.isMutating()) + + mutate1() + mutate2() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(queryClient.isMutating()) + + await vi.advanceTimersByTimeAsync(501) + isMutatingArray.push(queryClient.isMutating()) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(2) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpt1.mutationKey)', async () => { + const isMutatingArray: Array = [] + const queryClient = useQueryClient() + const mutationKey1 = ['mutation'] as const + const mutationOpts1 = mutationOptions({ + mutationKey: mutationKey1, + mutationFn: () => sleep(500).then(() => 'data1'), + }) + const mutationOpts2 = mutationOptions({ + mutationFn: () => sleep(500).then(() => 'data2'), + }) + + const { mutate: mutate1 } = useMutation(mutationOpts1) + const { mutate: mutate2 } = useMutation(mutationOpts2) + isMutatingArray.push(queryClient.isMutating({ + mutationKey: mutationKey1, + })) + + mutate1() + mutate2() + await vi.advanceTimersByTimeAsync(0) + isMutatingArray.push(queryClient.isMutating({ + mutationKey: mutationKey1, + })) + + await vi.advanceTimersByTimeAsync(501) + isMutatingArray.push(queryClient.isMutating({ + mutationKey: mutationKey1, + })) + + expect(isMutatingArray[0]).toEqual(0) + expect(isMutatingArray[1]).toEqual(1) + expect(isMutatingArray[2]).toEqual(0) + }) + + it('should return the number of fetching mutations when used with useMutationState (with mutationKey in mutationOptions)', async () => { + const queryClient = useQueryClient() + queryClient.clear() + const mutationStateArray: Array< + MutationState + > = [] + const mutationKey = ['mutation'] as const + const mutationOpts = mutationOptions({ + mutationKey, + mutationFn: () => sleep(10).then(() => 'data'), + }) + + const { mutate } = useMutation(mutationOpts) + const mutationState = useMutationState({ + filters: { mutationKey, status: 'success' }, + }) + + expect(mutationState.value.length).toEqual(0) + + mutate() + await vi.advanceTimersByTimeAsync(11) + + mutationStateArray.push(...mutationState.value) + expect(mutationStateArray.length).toEqual(1) + expect(mutationStateArray[0]?.data).toEqual('data') + }) + + it('should return the number of fetching mutations when used with useMutationState (without mutationKey in mutationOptions)', async () => { + const queryClient = useQueryClient() + queryClient.clear() + const mutationStateArray: Array< + MutationState + > = [] + const mutationOpts = mutationOptions({ + mutationFn: () => sleep(10).then(() => 'data'), + }) + + const { mutate } = useMutation(mutationOpts) + const mutationState = useMutationState({ + filters: { status: 'success' }, + }) + + expect(mutationState.value.length).toEqual(0) + + mutate() + await vi.advanceTimersByTimeAsync(11) + + mutationStateArray.push(...mutationState.value) + expect(mutationStateArray.length).toEqual(1) + expect(mutationStateArray[0]?.data).toEqual('data') + }) + + it('should return the number of fetching mutations when used with useMutationState', async () => { + const queryClient = useQueryClient() + queryClient.clear() + const mutationStateArray: Array< + MutationState + > = [] + const mutationOpts1 = mutationOptions({ + mutationKey: ['mutation'], + mutationFn: () => sleep(10).then(() => 'data1'), + }) + const mutationOpts2 = mutationOptions({ + mutationFn: () => sleep(10).then(() => 'data2'), + }) + + const { mutate: mutate1 } = useMutation(mutationOpts1) + const { mutate: mutate2 } = useMutation(mutationOpts2) + const mutationState = useMutationState({ + filters: { status: 'success' }, + }) + + expect(mutationState.value.length).toEqual(0) + + mutate1() + mutate2() + await vi.advanceTimersByTimeAsync(11) + + mutationStateArray.push(...mutationState.value) + expect(mutationStateArray.length).toEqual(2) + expect(mutationStateArray[0]?.data).toEqual('data1') + expect(mutationStateArray[1]?.data).toEqual('data2') + }) + + it('should return the number of fetching mutations when used with useMutationState (filter mutationOpt1.mutationKey)', async () => { + const queryClient = useQueryClient() + queryClient.clear() + const mutationStateArray: Array< + MutationState + > = [] + const mutationKey1 = ['mutation'] as const + const mutationOpts1 = mutationOptions({ + mutationKey: mutationKey1, + mutationFn: () => sleep(10).then(() => 'data1'), + }) + const mutationOpts2 = mutationOptions({ + mutationFn: () => sleep(10).then(() => 'data2'), + }) + + const { mutate: mutate1 } = useMutation(mutationOpts1) + const { mutate: mutate2 } = useMutation(mutationOpts2) + const mutationState = useMutationState({ + filters: { mutationKey: mutationKey1, status: 'success' }, + }) + + expect(mutationState.value.length).toEqual(0) + + mutate1() + mutate2() + await vi.advanceTimersByTimeAsync(11) + + mutationStateArray.push(...mutationState.value) + expect(mutationStateArray.length).toEqual(1) + expect(mutationStateArray[0]?.data).toEqual('data1') + expect(mutationStateArray[1]).toBeFalsy() + }) + + it('should stop listening to changes on onScopeDispose', async () => { + const onScopeDisposeMock = onScopeDispose as MockedFunction< + typeof onScopeDispose + > + onScopeDisposeMock.mockImplementation((fn) => fn()) + + const mutation = useMutation({ + mutationFn: (params: string) => sleep(0).then(() => params), + }) + const mutation2 = useMutation({ + mutationFn: (params: string) => sleep(0).then(() => params), + }) + const isMutating = useIsMutating() + + expect(isMutating.value).toStrictEqual(0) + + mutation.mutateAsync('a') + mutation2.mutateAsync('b') + + await vi.advanceTimersByTimeAsync(0) + + expect(isMutating.value).toStrictEqual(0) + + await vi.advanceTimersByTimeAsync(0) + + expect(isMutating.value).toStrictEqual(0) + + onScopeDisposeMock.mockReset() + }) +}) From fe97419b61a2faa19111c59f0ef0d2cb9b891acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B3=E3=82=B3=E3=83=AD?= <4946624+shincurry@users.noreply.github.com> Date: Wed, 14 Jan 2026 07:00:12 +0000 Subject: [PATCH 4/8] Add docs of mutationOptions --- docs/framework/vue/reference/mutationOptions.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/framework/vue/reference/mutationOptions.md diff --git a/docs/framework/vue/reference/mutationOptions.md b/docs/framework/vue/reference/mutationOptions.md new file mode 100644 index 0000000000..0fa145a890 --- /dev/null +++ b/docs/framework/vue/reference/mutationOptions.md @@ -0,0 +1,15 @@ +--- +id: mutationOptions +title: mutationOptions +--- + +```tsx +mutationOptions({ + mutationFn, + ...options, +}) +``` + +**Options** + +You can generally pass everything to `mutationOptions` that you can also pass to [`useMutation`](./useMutation.md). From a04bdffd857cb66bb79ae1473a7cbddf80263b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B3=E3=82=B3=E3=83=AD?= <4946624+shincurry@users.noreply.github.com> Date: Wed, 14 Jan 2026 08:43:01 +0000 Subject: [PATCH 5/8] Add changeset --- .changeset/lucky-numbers-change.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lucky-numbers-change.md diff --git a/.changeset/lucky-numbers-change.md b/.changeset/lucky-numbers-change.md new file mode 100644 index 0000000000..0933654041 --- /dev/null +++ b/.changeset/lucky-numbers-change.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-query': minor +--- + +Add mutationOptions. From f6da6bdbeb6198ba472f77ebed7741519350deb9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:13:01 +0000 Subject: [PATCH 6/8] ci: apply automated fixes --- .../src/__tests__/mutationOptions.test.ts | 24 ++++++++++++------- packages/vue-query/src/mutationOptions.ts | 18 ++++++++++---- packages/vue-query/src/types.ts | 4 +++- packages/vue-query/src/useMutation.ts | 4 +++- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/vue-query/src/__tests__/mutationOptions.test.ts b/packages/vue-query/src/__tests__/mutationOptions.test.ts index 74ce8c90c5..c2cf1bdaf5 100644 --- a/packages/vue-query/src/__tests__/mutationOptions.test.ts +++ b/packages/vue-query/src/__tests__/mutationOptions.test.ts @@ -232,21 +232,27 @@ describe('mutationOptions', () => { const { mutate: mutate1 } = useMutation(mutationOpts1) const { mutate: mutate2 } = useMutation(mutationOpts2) - isMutatingArray.push(queryClient.isMutating({ - mutationKey: mutationKey1, - })) + isMutatingArray.push( + queryClient.isMutating({ + mutationKey: mutationKey1, + }), + ) mutate1() mutate2() await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(queryClient.isMutating({ - mutationKey: mutationKey1, - })) + isMutatingArray.push( + queryClient.isMutating({ + mutationKey: mutationKey1, + }), + ) await vi.advanceTimersByTimeAsync(501) - isMutatingArray.push(queryClient.isMutating({ - mutationKey: mutationKey1, - })) + isMutatingArray.push( + queryClient.isMutating({ + mutationKey: mutationKey1, + }), + ) expect(isMutatingArray[0]).toEqual(0) expect(isMutatingArray[1]).toEqual(1) diff --git a/packages/vue-query/src/mutationOptions.ts b/packages/vue-query/src/mutationOptions.ts index 98dd12c01c..7619f36b90 100644 --- a/packages/vue-query/src/mutationOptions.ts +++ b/packages/vue-query/src/mutationOptions.ts @@ -1,4 +1,4 @@ -import type { UseMutationOptions } from './useMutation'; +import type { UseMutationOptions } from './useMutation' import type { DefaultError, WithRequired } from '@tanstack/query-core' import type { DeepUnwrapRefOrGetter, MaybeRefDeepOrGetter } from './types' @@ -10,13 +10,17 @@ export function mutationOptions< >( options: MaybeRefDeepOrGetter< WithRequired< - DeepUnwrapRefOrGetter>, + DeepUnwrapRefOrGetter< + UseMutationOptions + >, 'mutationKey' > >, ): MaybeRefDeepOrGetter< WithRequired< - DeepUnwrapRefOrGetter>, + DeepUnwrapRefOrGetter< + UseMutationOptions + >, 'mutationKey' > > @@ -28,13 +32,17 @@ export function mutationOptions< >( options: MaybeRefDeepOrGetter< Omit< - DeepUnwrapRefOrGetter>, + DeepUnwrapRefOrGetter< + UseMutationOptions + >, 'mutationKey' > >, ): MaybeRefDeepOrGetter< Omit< - DeepUnwrapRefOrGetter>, + DeepUnwrapRefOrGetter< + UseMutationOptions + >, 'mutationKey' > > diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts index 46ff03df2a..81c6d5831b 100644 --- a/packages/vue-query/src/types.ts +++ b/packages/vue-query/src/types.ts @@ -57,7 +57,9 @@ export type DeepUnwrapRef = T extends UnwrapLeaf } : UnwrapRef -export type DeepUnwrapRefOrGetter = T extends (...args: any) => MaybeRefDeep +export type DeepUnwrapRefOrGetter = T extends ( + ...args: any +) => MaybeRefDeep ? DeepUnwrapRef> : DeepUnwrapRef diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts index 39ccae9c8e..6481e6c8ce 100644 --- a/packages/vue-query/src/useMutation.ts +++ b/packages/vue-query/src/useMutation.ts @@ -39,7 +39,9 @@ export type UseMutationOptions< TError = DefaultError, TVariables = void, TOnMutateResult = unknown, -> = MaybeRefDeepOrGetter> +> = MaybeRefDeepOrGetter< + UseMutationOptionsBase +> type MutateSyncFunction< TData = unknown, From a93ad938003c5940c0f7ab07afb309319a07cf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B3=E3=82=B3=E3=83=AD?= <4946624+shincurry@users.noreply.github.com> Date: Tue, 27 Jan 2026 03:35:13 +0000 Subject: [PATCH 7/8] Remove unnecessary tests --- .../src/__tests__/mutationOptions.test.ts | 386 +----------------- 1 file changed, 1 insertion(+), 385 deletions(-) diff --git a/packages/vue-query/src/__tests__/mutationOptions.test.ts b/packages/vue-query/src/__tests__/mutationOptions.test.ts index c2cf1bdaf5..b1e5f3bde9 100644 --- a/packages/vue-query/src/__tests__/mutationOptions.test.ts +++ b/packages/vue-query/src/__tests__/mutationOptions.test.ts @@ -1,23 +1,8 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { onScopeDispose } from 'vue-demi' +import { describe, expect, it } from 'vitest' import { sleep } from '@tanstack/query-test-utils' import { mutationOptions } from '../mutationOptions' -import { useMutation } from '../useMutation' -import { useIsMutating, useMutationState } from '../useMutationState' -import { useQueryClient } from '../useQueryClient' -import type { MockedFunction } from 'vitest' -import type { MutationState } from '@tanstack/query-core' - -vi.mock('../useQueryClient') describe('mutationOptions', () => { - beforeEach(() => { - vi.useFakeTimers() - }) - - afterEach(() => { - vi.useRealTimers() - }) it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => { const object = { @@ -36,373 +21,4 @@ describe('mutationOptions', () => { expect(mutationOptions(object)).toStrictEqual(object) }) - it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => { - const isMutatingArray: Array = [] - const mutationOpts = mutationOptions({ - mutationKey: ['key'], - mutationFn: () => sleep(50).then(() => 'data'), - }) - - const { mutate } = useMutation(mutationOpts) - const isMutating = useIsMutating() - - isMutatingArray.push(isMutating.value) - - mutate() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(isMutating.value) - - await vi.advanceTimersByTimeAsync(51) - isMutatingArray.push(isMutating.value) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(1) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => { - const isMutatingArray: Array = [] - const mutationOpts = mutationOptions({ - mutationFn: () => sleep(50).then(() => 'data'), - }) - - const { mutate } = useMutation(mutationOpts) - const isMutating = useIsMutating() - - isMutatingArray.push(isMutating.value) - - mutate() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(isMutating.value) - - await vi.advanceTimersByTimeAsync(51) - isMutatingArray.push(isMutating.value) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(1) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with useIsMutating', async () => { - const isMutatingArray: Array = [] - const mutationOpts1 = mutationOptions({ - mutationKey: ['key'], - mutationFn: () => sleep(50).then(() => 'data1'), - }) - const mutationOpts2 = mutationOptions({ - mutationFn: () => sleep(50).then(() => 'data2'), - }) - - const { mutate: mutate1 } = useMutation(mutationOpts1) - const { mutate: mutate2 } = useMutation(mutationOpts2) - const isMutating = useIsMutating() - - isMutatingArray.push(isMutating.value) - - mutate1() - mutate2() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(isMutating.value) - - await vi.advanceTimersByTimeAsync(51) - isMutatingArray.push(isMutating.value) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(2) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => { - const isMutatingArray: Array = [] - const mutationKey1 = ['key'] as const - const mutationOpts1 = mutationOptions({ - mutationKey: mutationKey1, - mutationFn: () => sleep(50).then(() => 'data1'), - }) - const mutationOpts2 = mutationOptions({ - mutationFn: () => sleep(50).then(() => 'data2'), - }) - - const { mutate: mutate1 } = useMutation(mutationOpts1) - const { mutate: mutate2 } = useMutation(mutationOpts2) - const isMutating = useIsMutating({ - mutationKey: mutationKey1, - }) - - isMutatingArray.push(isMutating.value) - - mutate1() - mutate2() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(isMutating.value) - - await vi.advanceTimersByTimeAsync(51) - isMutatingArray.push(isMutating.value) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(1) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => { - const isMutatingArray: Array = [] - const queryClient = useQueryClient() - const mutationKey = ['mutation'] as const - const mutationOpts = mutationOptions({ - mutationKey, - mutationFn: () => sleep(500).then(() => 'data'), - }) - - const { mutate } = useMutation(mutationOpts) - isMutatingArray.push(queryClient.isMutating({ mutationKey })) - - mutate() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(queryClient.isMutating({ mutationKey })) - - await vi.advanceTimersByTimeAsync(501) - isMutatingArray.push(queryClient.isMutating({ mutationKey })) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(1) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => { - const isMutatingArray: Array = [] - const queryClient = useQueryClient() - const mutationOpts = mutationOptions({ - mutationFn: () => sleep(500).then(() => 'data'), - }) - - const { mutate } = useMutation(mutationOpts) - isMutatingArray.push(queryClient.isMutating()) - - mutate() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(queryClient.isMutating()) - - await vi.advanceTimersByTimeAsync(501) - isMutatingArray.push(queryClient.isMutating()) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(1) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with queryClient.isMutating', async () => { - const isMutatingArray: Array = [] - const queryClient = useQueryClient() - const mutationOpts1 = mutationOptions({ - mutationKey: ['mutation'], - mutationFn: () => sleep(500).then(() => 'data1'), - }) - const mutationOpts2 = mutationOptions({ - mutationFn: () => sleep(500).then(() => 'data2'), - }) - - const { mutate: mutate1 } = useMutation(mutationOpts1) - const { mutate: mutate2 } = useMutation(mutationOpts2) - isMutatingArray.push(queryClient.isMutating()) - - mutate1() - mutate2() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push(queryClient.isMutating()) - - await vi.advanceTimersByTimeAsync(501) - isMutatingArray.push(queryClient.isMutating()) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(2) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpt1.mutationKey)', async () => { - const isMutatingArray: Array = [] - const queryClient = useQueryClient() - const mutationKey1 = ['mutation'] as const - const mutationOpts1 = mutationOptions({ - mutationKey: mutationKey1, - mutationFn: () => sleep(500).then(() => 'data1'), - }) - const mutationOpts2 = mutationOptions({ - mutationFn: () => sleep(500).then(() => 'data2'), - }) - - const { mutate: mutate1 } = useMutation(mutationOpts1) - const { mutate: mutate2 } = useMutation(mutationOpts2) - isMutatingArray.push( - queryClient.isMutating({ - mutationKey: mutationKey1, - }), - ) - - mutate1() - mutate2() - await vi.advanceTimersByTimeAsync(0) - isMutatingArray.push( - queryClient.isMutating({ - mutationKey: mutationKey1, - }), - ) - - await vi.advanceTimersByTimeAsync(501) - isMutatingArray.push( - queryClient.isMutating({ - mutationKey: mutationKey1, - }), - ) - - expect(isMutatingArray[0]).toEqual(0) - expect(isMutatingArray[1]).toEqual(1) - expect(isMutatingArray[2]).toEqual(0) - }) - - it('should return the number of fetching mutations when used with useMutationState (with mutationKey in mutationOptions)', async () => { - const queryClient = useQueryClient() - queryClient.clear() - const mutationStateArray: Array< - MutationState - > = [] - const mutationKey = ['mutation'] as const - const mutationOpts = mutationOptions({ - mutationKey, - mutationFn: () => sleep(10).then(() => 'data'), - }) - - const { mutate } = useMutation(mutationOpts) - const mutationState = useMutationState({ - filters: { mutationKey, status: 'success' }, - }) - - expect(mutationState.value.length).toEqual(0) - - mutate() - await vi.advanceTimersByTimeAsync(11) - - mutationStateArray.push(...mutationState.value) - expect(mutationStateArray.length).toEqual(1) - expect(mutationStateArray[0]?.data).toEqual('data') - }) - - it('should return the number of fetching mutations when used with useMutationState (without mutationKey in mutationOptions)', async () => { - const queryClient = useQueryClient() - queryClient.clear() - const mutationStateArray: Array< - MutationState - > = [] - const mutationOpts = mutationOptions({ - mutationFn: () => sleep(10).then(() => 'data'), - }) - - const { mutate } = useMutation(mutationOpts) - const mutationState = useMutationState({ - filters: { status: 'success' }, - }) - - expect(mutationState.value.length).toEqual(0) - - mutate() - await vi.advanceTimersByTimeAsync(11) - - mutationStateArray.push(...mutationState.value) - expect(mutationStateArray.length).toEqual(1) - expect(mutationStateArray[0]?.data).toEqual('data') - }) - - it('should return the number of fetching mutations when used with useMutationState', async () => { - const queryClient = useQueryClient() - queryClient.clear() - const mutationStateArray: Array< - MutationState - > = [] - const mutationOpts1 = mutationOptions({ - mutationKey: ['mutation'], - mutationFn: () => sleep(10).then(() => 'data1'), - }) - const mutationOpts2 = mutationOptions({ - mutationFn: () => sleep(10).then(() => 'data2'), - }) - - const { mutate: mutate1 } = useMutation(mutationOpts1) - const { mutate: mutate2 } = useMutation(mutationOpts2) - const mutationState = useMutationState({ - filters: { status: 'success' }, - }) - - expect(mutationState.value.length).toEqual(0) - - mutate1() - mutate2() - await vi.advanceTimersByTimeAsync(11) - - mutationStateArray.push(...mutationState.value) - expect(mutationStateArray.length).toEqual(2) - expect(mutationStateArray[0]?.data).toEqual('data1') - expect(mutationStateArray[1]?.data).toEqual('data2') - }) - - it('should return the number of fetching mutations when used with useMutationState (filter mutationOpt1.mutationKey)', async () => { - const queryClient = useQueryClient() - queryClient.clear() - const mutationStateArray: Array< - MutationState - > = [] - const mutationKey1 = ['mutation'] as const - const mutationOpts1 = mutationOptions({ - mutationKey: mutationKey1, - mutationFn: () => sleep(10).then(() => 'data1'), - }) - const mutationOpts2 = mutationOptions({ - mutationFn: () => sleep(10).then(() => 'data2'), - }) - - const { mutate: mutate1 } = useMutation(mutationOpts1) - const { mutate: mutate2 } = useMutation(mutationOpts2) - const mutationState = useMutationState({ - filters: { mutationKey: mutationKey1, status: 'success' }, - }) - - expect(mutationState.value.length).toEqual(0) - - mutate1() - mutate2() - await vi.advanceTimersByTimeAsync(11) - - mutationStateArray.push(...mutationState.value) - expect(mutationStateArray.length).toEqual(1) - expect(mutationStateArray[0]?.data).toEqual('data1') - expect(mutationStateArray[1]).toBeFalsy() - }) - - it('should stop listening to changes on onScopeDispose', async () => { - const onScopeDisposeMock = onScopeDispose as MockedFunction< - typeof onScopeDispose - > - onScopeDisposeMock.mockImplementation((fn) => fn()) - - const mutation = useMutation({ - mutationFn: (params: string) => sleep(0).then(() => params), - }) - const mutation2 = useMutation({ - mutationFn: (params: string) => sleep(0).then(() => params), - }) - const isMutating = useIsMutating() - - expect(isMutating.value).toStrictEqual(0) - - mutation.mutateAsync('a') - mutation2.mutateAsync('b') - - await vi.advanceTimersByTimeAsync(0) - - expect(isMutating.value).toStrictEqual(0) - - await vi.advanceTimersByTimeAsync(0) - - expect(isMutating.value).toStrictEqual(0) - - onScopeDisposeMock.mockReset() - }) }) From 2210327ee20161385276820ffdf3cc2d56366af3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:45:42 +0000 Subject: [PATCH 8/8] ci: apply automated fixes --- packages/vue-query/src/__tests__/mutationOptions.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/vue-query/src/__tests__/mutationOptions.test.ts b/packages/vue-query/src/__tests__/mutationOptions.test.ts index b1e5f3bde9..9ed7996a66 100644 --- a/packages/vue-query/src/__tests__/mutationOptions.test.ts +++ b/packages/vue-query/src/__tests__/mutationOptions.test.ts @@ -3,7 +3,6 @@ import { sleep } from '@tanstack/query-test-utils' import { mutationOptions } from '../mutationOptions' describe('mutationOptions', () => { - it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => { const object = { mutationKey: ['key'], @@ -20,5 +19,4 @@ describe('mutationOptions', () => { expect(mutationOptions(object)).toStrictEqual(object) }) - })