Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-numbers-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-query': minor
---

Add mutationOptions.
15 changes: 15 additions & 0 deletions docs/framework/vue/reference/mutationOptions.md
Original file line number Diff line number Diff line change
@@ -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).
164 changes: 164 additions & 0 deletions packages/vue-query/src/__tests__/mutationOptions.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<number>()
},
})
})

it('should infer types for callbacks', () => {
mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer types for onError callback', () => {
mutationOptions({
mutationFn: () => {
throw new Error('fail')
},
mutationKey: ['key'],
onError: (error) => {
expectTypeOf(error).toEqualTypeOf<DefaultError>()
},
})
})

it('should infer types for variables', () => {
mutationOptions<number, DefaultError, { id: string }>({
mutationFn: (vars) => {
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
return Promise.resolve(5)
},
mutationKey: ['with-vars'],
})
})

it('should infer result type correctly', () => {
mutationOptions<number, DefaultError, void, { name: string }>({
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<number>({
mutationFn: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
return Promise.resolve(5)
},
mutationKey: ['key'],
onMutate: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
})
})

it('should error if mutationFn return type mismatches TData', () => {
assertType(
mutationOptions<number>({
// @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<number>()
},
})
})

it('should infer types when used with useMutation', () => {
const mutation = reactive(
useMutation(
mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
),
)
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()

reactive(
useMutation(
// should allow when used with useMutation without mutationKey
mutationOptions({
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
),
)
})

it('should infer types when used with useIsMutating', () => {
const isMutating = useIsMutating({
mutationKey: ['key'],
})
expectTypeOf(isMutating.value).toEqualTypeOf<number>()
})

it('should infer types when used with useMutationState', () => {
const mutationState = useMutationState({
filters: {
mutationKey: ['key'],
},
})
expectTypeOf(mutationState.value).toEqualTypeOf<
Array<MutationState<unknown, Error, unknown, unknown>>
>()
})

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<number | undefined>()
})
})
22 changes: 22 additions & 0 deletions packages/vue-query/src/__tests__/mutationOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
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'],
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)
})
})
1 change: 1 addition & 0 deletions packages/vue-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
58 changes: 58 additions & 0 deletions packages/vue-query/src/mutationOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { UseMutationOptions } from './useMutation'
import type { DefaultError, WithRequired } from '@tanstack/query-core'
import type { DeepUnwrapRefOrGetter, MaybeRefDeepOrGetter } from './types'

export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
options: MaybeRefDeepOrGetter<
WithRequired<
DeepUnwrapRefOrGetter<
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>
>,
'mutationKey'
>
>,
): MaybeRefDeepOrGetter<
WithRequired<
DeepUnwrapRefOrGetter<
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>
>,
'mutationKey'
>
>
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
options: MaybeRefDeepOrGetter<
Omit<
DeepUnwrapRefOrGetter<
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>
>,
'mutationKey'
>
>,
): MaybeRefDeepOrGetter<
Omit<
DeepUnwrapRefOrGetter<
UseMutationOptions<TData, TError, TVariables, TOnMutateResult>
>,
'mutationKey'
>
>
export function mutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
): UseMutationOptions<TData, TError, TVariables, TOnMutateResult> {
return options
}
8 changes: 8 additions & 0 deletions packages/vue-query/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export type MaybeRefDeep<T> = MaybeRef<
: T
>

export type MaybeRefDeepOrGetter<T> = MaybeRefDeep<T> | (() => T)

export type NoUnknown<T> = Equal<unknown, T> extends true ? never : T

export type Equal<TTargetA, TTargetB> =
Expand All @@ -55,6 +57,12 @@ export type DeepUnwrapRef<T> = T extends UnwrapLeaf
}
: UnwrapRef<T>

export type DeepUnwrapRefOrGetter<T> = T extends (
...args: any
) => MaybeRefDeep<any>
? DeepUnwrapRef<ReturnType<T>>
: DeepUnwrapRef<T>

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.
Expand Down
12 changes: 4 additions & 8 deletions packages/vue-query/src/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TData, TError, TVariables, TOnMutateResult> =
Expand All @@ -39,13 +39,9 @@ export type UseMutationOptions<
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> =
| MaybeRefDeep<
UseMutationOptionsBase<TData, TError, TVariables, TOnMutateResult>
>
| (() => MaybeRefDeep<
UseMutationOptionsBase<TData, TError, TVariables, TOnMutateResult>
>)
> = MaybeRefDeepOrGetter<
UseMutationOptionsBase<TData, TError, TVariables, TOnMutateResult>
>

type MutateSyncFunction<
TData = unknown,
Expand Down
Loading