From 596896d89a05799256e9f0e019da3119a7adadab Mon Sep 17 00:00:00 2001 From: ubinquitous Date: Sun, 6 Apr 2025 19:23:46 +0900 Subject: [PATCH 1/2] feat(react-query): add mutationOptions --- .../react/reference/mutationOptions.md | 15 +++ docs/framework/react/typescript.md | 18 +++ .../src/__tests__/mutationOptions.test-d.tsx | 23 ++++ .../src/__tests__/mutationOptions.test.tsx | 14 +++ packages/react-query/src/mutationOptions.ts | 105 ++++++++++++++++++ 5 files changed, 175 insertions(+) create mode 100644 docs/framework/react/reference/mutationOptions.md create mode 100644 packages/react-query/src/__tests__/mutationOptions.test-d.tsx create mode 100644 packages/react-query/src/__tests__/mutationOptions.test.tsx create mode 100644 packages/react-query/src/mutationOptions.ts 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(['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..9f43c117d3 --- /dev/null +++ b/packages/react-query/src/__tests__/mutationOptions.test-d.tsx @@ -0,0 +1,23 @@ +import { describe, expectTypeOf, it } from 'vitest' +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() + }, + }) + }) +}) 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 & { + initialData?: + | undefined + | InitialDataFunction> + | NonUndefinedGuard +} + +export type UnusedSkipTokenOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +> = OmitKeyof< + UseMutationOptions, + 'mutationFn' +> & { + mutationFn?: Exclude< + UseMutationOptions< + TMutationFnData, + TError, + TData, + TMutationKey + >['mutationFn'], + SkipToken | undefined + > +} + +type NonUndefinedGuard = T extends undefined ? never : T + +export type DefinedInitialDataOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +> = Omit< + UseMutationOptions, + 'mutationFn' +> & { + initialData: + | NonUndefinedGuard + | (() => NonUndefinedGuard) + mutationFn?: MutationFunction +} + +export function mutationOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +>( + options: DefinedInitialDataOptions< + TMutationFnData, + TError, + TData, + TMutationKey + >, +): DefinedInitialDataOptions & { + mutationKey: DataTag +} + +export function mutationOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +>( + options: UnusedSkipTokenOptions, +): UnusedSkipTokenOptions & { + mutationKey: DataTag +} + +export function mutationOptions< + TMutationFnData = unknown, + TError = DefaultError, + TData = void, + TMutationKey = unknown, +>( + options: UndefinedInitialDataOptions< + TMutationFnData, + TError, + TData, + TMutationKey + >, +): UndefinedInitialDataOptions & { + mutationKey: DataTag +} + +export function mutationOptions(options: unknown) { + return options +} From ff15e5d33c24f1d0067db745989a27927c1283a6 Mon Sep 17 00:00:00 2001 From: ubinquitous Date: Mon, 7 Apr 2025 17:14:42 +0900 Subject: [PATCH 2/2] test(react-query): add DataTag test case --- .../src/__tests__/mutationOptions.test-d.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/react-query/src/__tests__/mutationOptions.test-d.tsx b/packages/react-query/src/__tests__/mutationOptions.test-d.tsx index 9f43c117d3..e16d3ac302 100644 --- a/packages/react-query/src/__tests__/mutationOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/mutationOptions.test-d.tsx @@ -1,4 +1,5 @@ import { describe, expectTypeOf, it } from 'vitest' +import { dataTagSymbol } from '@tanstack/query-core' import { mutationOptions } from '../mutationOptions' describe('mutationOptions', () => { @@ -20,4 +21,31 @@ describe('mutationOptions', () => { }, }) }) + + 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() + }) + + it('should tag the mutationKey with unknown if there is no mutationFn', () => { + const { mutationKey } = mutationOptions({ + mutationKey: ['key'], + }) + + expectTypeOf(mutationKey[dataTagSymbol]).toEqualTypeOf() + }) + + 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() + }) })