diff --git a/docs/framework/react/reference/mutationOptions.md b/docs/framework/react/reference/mutationOptions.md new file mode 100644 index 0000000000..0fa145a890 --- /dev/null +++ b/docs/framework/react/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). diff --git a/docs/framework/react/typescript.md b/docs/framework/react/typescript.md index baac2cc771..74a07c8101 100644 --- a/docs/framework/react/typescript.md +++ b/docs/framework/react/typescript.md @@ -239,6 +239,24 @@ const data = queryClient.getQueryData<Group[]>(['groups']) [//]: # 'TypingQueryOptions' [//]: # 'Materials' +## Typing Mutation Options + +Similarly to `queryOptions`, you can use `mutationOptions` to extract mutation options into a separate function: + +```ts +function useGroupPostMutation() { + const queryClient = useQueryClient() + + return mutationOptions({ + mutationKey: ['groups'], + mutationFn: executeGroups, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['posts'] }) + }, + }) +} +``` + ## Further Reading For tips and tricks around type inference, have a look at [React Query and TypeScript](./community/tkdodos-blog.md#6-react-query-and-typescript) from diff --git a/packages/react-query/src/__tests__/mutationOptions.test-d.tsx b/packages/react-query/src/__tests__/mutationOptions.test-d.tsx new file mode 100644 index 0000000000..e16d3ac302 --- /dev/null +++ b/packages/react-query/src/__tests__/mutationOptions.test-d.tsx @@ -0,0 +1,51 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { dataTagSymbol } from '@tanstack/query-core' +import { mutationOptions } from '../mutationOptions' + +describe('mutationOptions', () => { + it('should not allow excess properties', () => { + return mutationOptions({ + mutationFn: () => Promise.resolve(5), + mutationKey: ['key'], + // @ts-expect-error this is a good error, because onMutates does not exist! + onMutates: 1000, + }) + }) + + it('should infer types for callbacks', () => { + return mutationOptions({ + mutationFn: () => Promise.resolve(5), + mutationKey: ['key'], + onSuccess: (data) => { + expectTypeOf(data).toEqualTypeOf<number>() + }, + }) + }) + + it('should tag the mutationKey with the result type of the MutationFn', () => { + const { mutationKey } = mutationOptions({ + mutationKey: ['key'], + mutationFn: () => Promise.resolve(5), + }) + + expectTypeOf(mutationKey[dataTagSymbol]).toEqualTypeOf<number>() + }) + + it('should tag the mutationKey with unknown if there is no mutationFn', () => { + const { mutationKey } = mutationOptions({ + mutationKey: ['key'], + }) + + expectTypeOf(mutationKey[dataTagSymbol]).toEqualTypeOf<unknown>() + }) + + it('should tag the mutationKey with the result type of the MutationFn if onSuccess is used', () => { + const { mutationKey } = mutationOptions({ + mutationKey: ['key'], + mutationFn: () => Promise.resolve(5), + onSuccess: () => {}, + }) + + expectTypeOf(mutationKey[dataTagSymbol]).toEqualTypeOf<number>() + }) +}) diff --git a/packages/react-query/src/__tests__/mutationOptions.test.tsx b/packages/react-query/src/__tests__/mutationOptions.test.tsx new file mode 100644 index 0000000000..cea9683491 --- /dev/null +++ b/packages/react-query/src/__tests__/mutationOptions.test.tsx @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { mutationOptions } from '../mutationOptions' +import type { UseMutationOptions } from '../types' + +describe('mutationOptions', () => { + it('should return the object received as a parameter without any modification.', () => { + const object: UseMutationOptions = { + mutationKey: ['key'], + mutationFn: () => Promise.resolve(5), + } as const + + expect(mutationOptions(object)).toStrictEqual(object) + }) +}) diff --git a/packages/react-query/src/mutationOptions.ts b/packages/react-query/src/mutationOptions.ts new file mode 100644 index 0000000000..a37c9d745a --- /dev/null +++ b/packages/react-query/src/mutationOptions.ts @@ -0,0 +1,105 @@ +import type { + DataTag, + DefaultError, + InitialDataFunction, + MutationFunction, + OmitKeyof, + SkipToken, +} from '@tanstack/query-core' +import type { UseMutationOptions } from './types' + +export type UndefinedInitialDataOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +> = UseMutationOptions<TMutationFnData, TError, TData, TMutationKey> & { + initialData?: + | undefined + | InitialDataFunction<NonUndefinedGuard<TMutationFnData>> + | NonUndefinedGuard<TMutationFnData> +} + +export type UnusedSkipTokenOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +> = OmitKeyof< + UseMutationOptions<TMutationFnData, TError, TData, TMutationKey>, + 'mutationFn' +> & { + mutationFn?: Exclude< + UseMutationOptions< + TMutationFnData, + TError, + TData, + TMutationKey + >['mutationFn'], + SkipToken | undefined + > +} + +type NonUndefinedGuard<T> = T extends undefined ? never : T + +export type DefinedInitialDataOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +> = Omit< + UseMutationOptions<TMutationFnData, TError, TData, TMutationKey>, + 'mutationFn' +> & { + initialData: + | NonUndefinedGuard<TMutationFnData> + | (() => NonUndefinedGuard<TMutationFnData>) + mutationFn?: MutationFunction<TMutationFnData, TMutationKey> +} + +export function mutationOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +>( + options: DefinedInitialDataOptions< + TMutationFnData, + TError, + TData, + TMutationKey + >, +): DefinedInitialDataOptions<TMutationFnData, TError, TData, TMutationKey> & { + mutationKey: DataTag<TMutationKey, TMutationFnData, TError> +} + +export function mutationOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +>( + options: UnusedSkipTokenOptions<TMutationFnData, TError, TData, TMutationKey>, +): UnusedSkipTokenOptions<TMutationFnData, TError, TData, TMutationKey> & { + mutationKey: DataTag<TMutationKey, TMutationFnData, TError> +} + +export function mutationOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +>( + options: UndefinedInitialDataOptions< + TMutationFnData, + TError, + TData, + TMutationKey + >, +): UndefinedInitialDataOptions<TMutationFnData, TError, TData, TMutationKey> & { + mutationKey: DataTag<TMutationKey, TMutationFnData, TError> +} + +export function mutationOptions(options: unknown) { + return options +}